def test_effective_principals_for_user( business_unit, department, have_direct_reports, has_direct_reports, is_staff, is_talent_manager, expected, ): policy = security.SimpleAuthenticationPolicy() request_mock = mock.MagicMock() direct_reports = [User(username="******")] if have_direct_reports else [] User.direct_reports = mock.MagicMock() request_mock.user = User( username=TEST_USERNAME, first_name="Foo", last_name="Bar", position="Tester", manager_username="******", department=department, employee_id=1, business_unit=business_unit, location="Planet Earth", email="*****@*****.**", has_direct_reports=has_direct_reports, is_staff=is_staff, ) request_mock.user.direct_reports = direct_reports request_mock.registry = mock.MagicMock() talent_managers = [TEST_USERNAME] if is_talent_manager else [] request_mock.registry.settings = { constants.TALENT_MANAGER_USERNAMES_KEY: talent_managers, constants.BUSINESS_UNIT_KEY: "Alpha", } assert expected == policy.effective_principals(request_mock)
def test_employee_can_get_blank_feedback_form_to_valid_nominee_while_inside_entry_subperiod( app_with_nominees_inside_entry_subperiod): # noqa: E501 app = successfully_login(app_with_nominees_inside_entry_subperiod, TEST_EMPLOYEE_USERNAME) response = app.get(NOMINATED_USER_FEEDBACK_ENDPOINT) form = response.json_body.get("form") end_date = form["endDate"] # note in entry subperiod for `generate_period_dates` hence 1 day offset man_location = TEST_LDAP_FULL_DETAILS[TEST_EMPLOYEE_USERNAME][ LDAP_LOCATION_ATTR] assert end_date == datetimeformat(TEST_UTCNOW + timedelta(days=1), User(location=man_location)) items = form["items"] assert isinstance(items, list) employee = form["employee"] assert employee["displayName"] == NOMINATED_DISPLAY_NAME assert employee["position"] == NOMINATED_POSITION expected_questions = [] for _, template, caption in QUESTION_IDS_AND_TEMPLATES: expected_questions.append([ template.format(period_name=TEST_PERIOD_NAME, display_name=NOMINATED_DISPLAY_NAME), caption, ]) for generated, expected in zip(items, expected_questions): assert generated["questionId"] assert generated["question"] == expected[0] assert generated["answerId"] is None assert generated["answer"] == "" assert generated["caption"] == expected[1]
def _generate_user_list(): ldapsource = ldapauth.build_ldapauth_from_settings(DEFAULT_TEST_SETTINGS) users = [] for rlu in TEST_LDAP_FULL_DETAILS.values(): u = User.create_from_ldap_details(ldapsource, rlu) users.append(u) return sorted(users, key=lambda u: (u.last_name, u.first_name))
def test_population_validation_catches_duplicate_users(): def _get_ldap_user_by_employee_id_mck(k, id_): if k != TEST_UID_KEY: pytest.fail('Incorrect key "{}" provided'.format(k)) details_by_id = {v[TEST_UID_KEY]: v for _, v in TEST_LDAP_FULL_DETAILS.items()} return details_by_id.get(id_) with mock.patch( "adaero.security.ldapauth.LDAPAuth." "get_ldap_user_by_kv", side_effect=_get_ldap_user_by_employee_id_mck, ): ldapsource = ldapauth.build_ldapauth_from_settings(DEFAULT_TEST_SETTINGS) input_ = _generate_user_list() duplicate_user = User.create_from_ldap_details( ldapsource, TEST_LDAP_FULL_DETAILS[NOMINATED_USERNAME] ) duplicate_user.first_name = "Duplicate" input_.append(duplicate_user) messages = [] generated, messages = population.remove_duplicate_users(input_, messages) assert 1 == len(messages) # take into account the header row duplicate_row_num = len(TEST_LDAP_FULL_DETAILS.keys()) + 2 assert ( population.DEFAULT_DUPLICATE_USER_TMPL.format( row_num=duplicate_row_num, first_row_num=USER_TO_REMOVE_ROW_NUM ) == messages[0] ) expected = _generate_user_list() for e, g in zip(expected, generated): assert e.first_name == e.first_name
def new_ldap_mocked_app_with_users(dbsession, request): settings = DEFAULT_TEST_SETTINGS ldapsource = ldapauth.build_ldapauth_from_settings(DEFAULT_TEST_SETTINGS) if request.config.getoption("--use-sqlite3"): settings["adaero.use_local_sqlite3"] = True # need yield_fixture as we need the patch applied over the lifetime of # the testapp instance with patch( "adaero.security.ldapauth.LDAPAuth.auth_user", side_effect=auth_user_mock_fn, autospec=True, ), patch( "adaero.security.ldapauth.LDAPAuth." "get_ldap_user_by_username", side_effect=get_ldap_user_by_username_mock_fn, autospec=True, ), patch( "adaero.security.ldapauth.LDAPAuth." "get_ldap_user_by_email", side_effect=get_ldap_by_email_mock_fn, autospec=True, ): app = webtest.TestApp(adaero.main({}, **settings)) dbsession = get_dbsession(app) with transaction.manager: for user_details in TEST_LDAP_FULL_DETAILS.values(): if (user_details[tests.integration.constants.TEST_USERNAME_KEY] not in TEST_EMPLOYEES): continue user = User.create_from_ldap_details(ldapsource, user_details) set_as_staff(user, user_details) dbsession.add(user) # Add non-staff member e.g. upper management non_staff_user = User.create_from_ldap_details( ldapsource, TEST_NON_STAFF_USER) dbsession.add(non_staff_user) freezer = freeze_time(TEST_UTCNOW) freezer.start() yield app freezer.stop()
def _generate_user_rows(): ldapsource = ldapauth.build_ldapauth_from_settings(DEFAULT_TEST_SETTINGS) user_rows = [] for rlu in TEST_LDAP_FULL_DETAILS.values(): u = User.create_from_ldap_details(ldapsource, rlu) u.is_staff = True user_rows.append(u.to_dict()) return sorted(user_rows, key=lambda u: (u["last_name"], u["first_name"]))
def stats_session(dbsession): ldapsource = ldapauth.build_ldapauth_from_settings(DEFAULT_TEST_SETTINGS) with transaction.manager: for user_details in TEST_LDAP_FULL_DETAILS.values(): dbsession.add( User.create_from_ldap_details(ldapsource, user_details)) add_test_data_for_stats(dbsession) yield dbsession drop_everything(dbsession)
def get_external_invite_status(request): """ Generate list of existing external invites that `request.user` has already sent for the current period. Parameters ---------- request: `pyramid.request.Request` Returns ------- JSON-serialisable dict that contains list of external users invited. """ ldapsource = request.ldapsource location = get_config_value(request.registry.settings, constants.HOMEBASE_LOCATION_KEY) current_period = Period.get_current_period(request.dbsession) if current_period.subperiod(location) in [ Period.APPROVAL_SUBPERIOD, Period.REVIEW_SUBPERIOD, ]: return interpolate_template(ENTRY_ENDED_TEMPLATE) elif current_period.subperiod(location) in [Period.ENROLLMENT_SUBPERIOD]: dt = date.datetimeformat(current_period.entry_start_utc, request.user) return interpolate_template(PRIOR_ENTRY_TEMPLATE, entry_start=dt) with transaction.manager: is_nominated = (request.dbsession.query(Nominee).filter( Nominee.period == current_period).filter( Nominee.username == request.user.username).one_or_none()) if not is_nominated: return interpolate_template(NOT_ENROLLED_TEMPLATE) invitee_users = [] with transaction.manager: invites = (request.dbsession.query(ExternalInvite.to_username).filter( ExternalInvite.from_username == request.user.username, ExternalInvite.period_id == current_period.id, ).all()) for invitee in invites: ldap_details = ldapsource.get_ldap_user_by_username(invitee[0]) invitee_users.append( User.create_from_ldap_details(ldapsource, ldap_details)) invitee_users.sort(key=lambda x: x.first_name) payload_users = [] for user in invitee_users: payload_users.append({ "displayName": user.display_name, "businessUnit": user.business_unit, "department": user.department, "email": user.email, }) return {"canInvite": True, "invitees": payload_users}
def post_external_invite(request): """ If allowed, records that `request.user` sent an external invite to a user identified by their email which must be in LDAP, and sends an email to that invited used. Parameters ---------- request: `pyramid.request.Request` Returns ------- JSON serialisable payload stating success """ ldapsource = request.ldapsource email = request.json_body["email"] location = get_config_value(request.registry.settings, constants.HOMEBASE_LOCATION_KEY) current_period = Period.get_current_period(request.dbsession) settings = request.registry.settings company_name = get_config_value(settings, constants.COMPANY_NAME_KEY, "company") support_email = get_config_value(settings, constants.SUPPORT_EMAIL_KEY, "your IT support for this tool") with transaction.manager: if current_period.subperiod(location) != Period.ENTRY_SUBPERIOD: raise HTTPBadRequest(explanation="Can only send invite during " 'the "Give feedback" period.') ext_user_details = ldapsource.get_ldap_user_by_email(email) if not ext_user_details: raise HTTPNotFound(explanation="%s is not a valid %s " "email. If you think it is, " "please contact " "%s." % (email, company_name, support_email)) ext_user = User.create_from_ldap_details(ldapsource, ext_user_details) invite = ExternalInvite( to_username=ext_user.username, from_username=request.user.username, period=current_period, ) invites = (request.dbsession.query(ExternalInvite).filter( ExternalInvite.to_username == ext_user.username, ExternalInvite.from_username == request.user.username, ExternalInvite.period_id == current_period.id, ).one_or_none()) if not invites: request.dbsession.add(invite) mail.send_invite_email( request.dbsession, request.registry.settings, inviter=request.user, invitee=ext_user, ) return {"success": True}
def test_manager_can_get_initial_summary_for_direct_report( ldap_mocked_app_with_users): # noqa: E501 app = successfully_login(ldap_mocked_app_with_users, TEST_MANAGER_USERNAME) dbsession = get_dbsession(app) add_test_data_for_stats(dbsession) response_1 = app.get("/api/v1/summarise/%s/" % TEST_EMPLOYEE_USERNAME) period_name = response_1.json_body["summary"]["periodName"] assert TEST_PERIOD_NAME == period_name assert not response_1.json_body["summary"]["readOnly"] end_date = response_1.json_body["summary"]["endDate"] # note in entry subperiod for `generate_period_dates` hence 1 day offset man_location = TEST_LDAP_FULL_DETAILS[TEST_EMPLOYEE_USERNAME][ LDAP_LOCATION_ATTR] assert end_date == datetimeformat(TEST_UTCNOW + timedelta(days=1), User(location=man_location)) # can't easily test summaries by direct string comparison, so instead: # 1. make sure number of `rawSummary` rows equal number of forms # recevied by the direct report for the CURRENT PERIOD only first_raw_summary = response_1.json_body["summary"]["items"][0][ "rawAnswer"] # below assertion only works in test situations as we ensure answers # have no new line, but in production, this will not be the case assert len(first_raw_summary.split("\n")) == TEST_NUM_FORMS_RECEIVED response_2 = app.get("/api/v1/summarise/%s/" % TEST_EMPLOYEE_USERNAME) items_1 = sorted(response_1.json_body["summary"]["items"], key=lambda k: k["questionId"]) items_2 = sorted(response_2.json_body["summary"]["items"], key=lambda k: k["questionId"]) assert len(items_1) == len(items_2) for i, j in zip(items_1, items_2): assert i["questionId"] == j["questionId"] assert i["question"] == j["question"] assert i["rawAnswer"] == j["rawAnswer"]
def check_and_send_email( dbsession, ldapsource, settings, template_key=None, force=False, delay_s=None, delay_between_s=None, ): """ Check current conditions and if we haven't sent the relevant email, send templated both plain text and HTML content to all relevant email addresses using the configured SMTP server. Parameters ---------- dbsession: sqlalchemy session ldapsource: used for fetching talent manager email information settings: configpaste settings template_key: override relevant email by providing key from `adaero.constants.EMAIL_TEMPLATE_MAP` force: if particular email already sent, send anyway delay_s: number of seconds to delay before sending emails. If none, look in settings delay_between_s: number of seconds to delay before sending emails. If none, look in settings """ log.info("Begin: Sending emails") current_period = Period.get_current_period(dbsession) location = get_config_value(settings, constants.HOMEBASE_LOCATION_KEY) if template_key: template_info = constants.EMAIL_TEMPLATE_MAP[template_key] log.info("Email template overriden to %s" % template_info["code"]) else: template_info = current_period.current_email_template(location) if not template_info: log.warning("Attempted to send an email while period is inactive") return last_sent = current_period.get_email_flag_by_code(template_info["code"]) if not force and last_sent: log.warning("Email code %s already sent at %s so not doing again, " "override with `force=True` kwarg." % (template_info["code"], last_sent)) return audience = template_info["audience"] company_name = get_config_value(settings, constants.COMPANY_NAME_KEY, "") subject = _build_full_subject(company_name, template_info["summary"]) if audience == "employee": users = get_employee_users(dbsession) elif audience == "non-nominated": users = get_non_nominated_users(dbsession) elif audience == "manager": users = get_manager_users(dbsession) elif audience == "summarised": users = get_summarised_users(dbsession) else: raise ValueError('Audience value "%s" not in allowed values "%s". ' "Please alert the application maintainer." % (audience, ", ".join(constants.AUDIENCE_VALUES))) emailing_enabled = _get_send_email_flag(settings) app_host = get_root_url(settings) # calculate email stats users_with_emails = [] for user in users: if not user.email: log.warning( "Unable to send email for user %s as no email available" % user.username) else: users_with_emails.append(user) if delay_s is None: delay_s = float( get_config_value(settings, constants.EMAIL_START_DELAY_S_KEY, DEFAULT_EMAIL_DELAY_S)) if delay_between_s is None: delay_between_s = float( get_config_value( settings, constants.EMAIL_DELAY_BETWEEN_S_KEY, DEFAULT_EMAIL_DELAY_BETWEEN_S, )) log.info('Sending %s "%s" emails in %s seconds...' % (template_info["code"], len(users_with_emails), delay_s)) time.sleep(delay_s) log.info("Sending %s emails now..." % len(users_with_emails)) env = _build_template_env() have_sent_emails = False from_email = get_config_value(settings, constants.SUPPORT_EMAIL_KEY) s = smtplib.SMTP() s.connect() for user in users: if not user.email: continue try: # because of the modelling of User <-> Manager, attempting to fetch # manager directly despite being joinloaded will result in an SELECT # to prevent db access by testing against local manager_username template = env.get_template( os.path.join("email", template_info["template"])) rendered_html = template.render( user=user, period=current_period, app_host=app_host, company_name=company_name, ) message_root = _generate_message_root(rendered_html, from_email, subject) if emailing_enabled: s.sendmail(from_email, [user.email], message_root.as_string()) have_sent_emails = True log.debug("Email sent to %s" % user.email) except Exception as e: log.exception(e) log.error("Exception occured with sending email to %s, " "skipping over and continuing..." % user.email) time.sleep(delay_between_s) tm_usernames = settings[constants.TALENT_MANAGER_USERNAMES_KEY] if not isinstance(tm_usernames, list): talent_managers = json.loads( settings[constants.TALENT_MANAGER_USERNAMES_KEY]) else: talent_managers = settings[constants.TALENT_MANAGER_USERNAMES_KEY] for tm_username in talent_managers: try: tm_ldap = ldapsource.get_ldap_user_by_username(tm_username) if not tm_ldap: log.warning("Unable to find LDAP info for talent manager with " "username {}, unable to send confirmation " "email.".format(tm_username)) continue tm = User.create_from_ldap_details(ldapsource, tm_ldap) # send confirmation email template = env.get_template( os.path.join("email", "tm_confirmation.html.j2")) rendered_html = template.render( talent_manager=tm, subject=subject, num_emails=len(users_with_emails), datetime_sent_utc=datetime.utcnow(), app_host=app_host, ) message_root = _generate_message_root( rendered_html, from_email, _build_full_subject(company_name, "Emails sent"), ) if emailing_enabled and have_sent_emails: s.sendmail(from_email, [tm.email], message_root.as_string()) except Exception as e: log.exception(e) log.error("Exception occured with sending tm email to %s, " "skipping over and continuing..." % tm_username) s.close() log.info("Sent %s emails!" % (len(users_with_emails) + 1)) with transaction.manager: current_period.set_email_flag_by_code(template_info["code"]) dbsession.merge(current_period) log.info("End: Sending emails")
def test_employee_able_to_send_feedback_request_within_entry_subperiod( ldap_mocked_app_with_users, email, expected): # noqa: E501 expected_status_code, expected_msg = expected app = ldap_mocked_app_with_users ldapsource = ldapauth.build_ldapauth_from_settings( app.app.registry.settings) dbsession = get_dbsession(app) template_id = add_test_template(dbsession) add_test_period_with_template(dbsession, Period.ENTRY_SUBPERIOD, template_id) successfully_login(app, TEST_EMPLOYEE_USERNAME) csrf_token = app.cookies[ANGULAR_2_XSRF_TOKEN_COOKIE_NAME] app.app.registry.settings[app_constants.ENABLE_SEND_EMAIL_KEY] = True with patch("smtplib.SMTP") as smtp_mock, patch( "socket.gethostname") as gethostname_mock, patch( "getpass.getuser") as getuser_mock: sendmail_mock = smtp_mock().sendmail gethostname_mock.return_value = TEST_PRODUCTION_HOSTNAME getuser_mock.return_value = TEST_PRODUCTION_USER resp = app.post_json( "/api/v1/external-invite", {"email": email}, headers={ANGULAR_2_XSRF_TOKEN_HEADER_NAME: csrf_token}, expect_errors=expected_status_code != 200, ) if resp.status_code != 200: assert expected_msg == resp.json_body["message"] return # check invite email that is sent parser = Parser() assert len(sendmail_mock.call_args_list) == 1 raw_message = sendmail_mock.call_args_list[0][0][2] message_root = parser.parsestr(raw_message) inviter = User.create_from_ldap_details( ldapsource, TEST_LDAP_FULL_DETAILS[TEST_EMPLOYEE_USERNAME]) subject_str, encoding = decode_header(message_root["Subject"])[0] subject_unicode = subject_str.decode(encoding) assert subject_unicode.count(inviter.display_name) == 1 invite_messages = message_root.get_payload() assert invite_messages[0]["Reply-To"] == inviter.email invite_plain = b64decode(invite_messages[0].get_payload()).decode("utf-8") invite_html = b64decode(invite_messages[1].get_payload()).decode("utf-8") app_url = "https://%s/feedback/%s" % ( TEST_PRODUCTION_HOSTNAME, TEST_LDAP_FULL_DETAILS[inviter.username][ tests.integration.constants.TEST_USERNAME_KEY], ) assert invite_plain.count(app_url) > 0 assert invite_plain.count(inviter.first_name) > 0 assert invite_html.count(app_url) > 1 assert invite_html.count(app_url) % 2 == 0 after_invite_resp = app.post_json( "/api/v1/login", { "username": TEST_COMPANY_COLLEAGUE_USERNAME, "password": TEST_PASSWORD }, ) assert after_invite_resp.status_code == 200
def test_datetimeformat_works(value, man_location, format_, expected): user = User(username="******", location=man_location) if format_: assert expected == adaero.date.datetimeformat(value, user, format_) else: assert expected == adaero.date.datetimeformat(value, user)