def test_advice_job_boards_extra_data(self): """Test that the advisor computes extra data for the "Find a Job Board" advice.""" project = project_pb2.Project( target_job=job_pb2.Job(job_group=job_pb2.JobGroup( rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.jobboards.insert_one({ 'title': 'Indeed', 'filters': ['for-departement(14)'] }) self.database.advice_modules.insert_one({ 'adviceId': 'job-boards', 'triggerScoringModel': 'advice-job-boards', 'extraDataFieldName': 'job_boards_data', 'isReadyForProd': True, }) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'job-boards') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual('Indeed', advice.job_boards_data.job_board_title) self.assertFalse(advice.job_boards_data.is_specific_to_job_group) self.assertTrue(advice.job_boards_data.is_specific_to_region)
def test_advice_other_work_env_extra_data(self): """Test that the advisor computes extra data for the work environment advice.""" project = project_pb2.Project(target_job=job_pb2.Job( job_group=job_pb2.JobGroup(rome_id='A1234')), ) self.user.features_enabled.alpha = True self.database.job_group_info.insert_one({ '_id': 'A1234', 'workEnvironmentKeywords': { 'structures': ['A', 'B'], 'sectors': ['sector Toise', 'sector Gal'], }, }) self.database.advice_modules.insert_one({ 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'advice-other-work-env', 'extraDataFieldName': 'other_work_env_advice_data', 'isReadyForProd': True, }) advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'other-work-env') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual(['A', 'B'], advice.other_work_env_advice_data. work_environment_keywords.structures) self.assertEqual([ 'sector Toise', 'sector Gal' ], advice.other_work_env_advice_data.work_environment_keywords.sectors)
def test_incompatible_advice_modules(self, mock_send_template): """Test that the advisor discard incompatible advice modules.""" mock_send_template().status_code = 200 mock_send_template.reset_mock() project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'other-work-env', 'airtableId': 'abc', 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, 'incompatibleAdviceIds': ['def'], }, { 'adviceId': 'spontaneous-application', 'airtableId': 'def', 'triggerScoringModel': 'constant(3)', 'isReadyForProd': True, 'incompatibleAdviceIds': ['abc'], }, { 'adviceId': 'final-one', 'airtableId': 'ghi', 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['spontaneous-application', 'final-one'], [a.advice_id for a in project.advices]) mock_send_template.assert_called_once()
def test_no_diagnostic_if_project_incomplete(self): """The diagnostic does not get populated when the project is marked as incomplete.""" project = project_pb2.Project(is_incomplete=True) self.assertFalse( advisor.maybe_diagnose(self.user, project, self.database)) self.assertFalse(project.HasField('diagnostic'))
def test_categorize_advice_missing(self): """Test that the advisor does not choke on an ancient advice that does not exist anymore.""" project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), advices=[ project_pb2.Advice(advice_id='spontaneous-application', status=project_pb2.ADVICE_RECOMMENDED, num_stars=2), project_pb2.Advice(advice_id='advice-does-no-exist-anymore', status=project_pb2.ADVICE_RECOMMENDED, num_stars=2), ]) self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first', 'second'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, ]) advisor.maybe_categorize_advice(self.user, project, self.database) # Point check that some categorization happened. self.assertEqual(['spontaneous-application'], project.advice_categories[0].advice_ids)
def test_text_filtering_uses_bad_overall_score(self): """The filtering of sentences uses the bad overall score.""" project = project_pb2.Project() self.database.diagnostic_sentences.insert_many([ { 'sentenceTemplate': 'All good', 'order': 1, 'filters': ['for-good-overall-score(50)'], }, { 'sentenceTemplate': 'Bad', 'order': 1, }, ]) self.database.diagnostic_submetrics_sentences.insert_one({ '_id': 'recJ3ugOeIIM6BlN3', 'triggerScoringModel': 'constant(0)', 'positiveSentenceTemplate': "Vous avez de l'expérience.", 'submetric': 'PROFILE_DIAGNOSTIC', 'weight': 1, 'negativeSentenceTemplate': "Vous manquez d'expérience.", }) advisor.maybe_diagnose(self.user, project, self.database) self.assertEqual('Bad', project.diagnostic.text)
def test_is_for_alpha_only(self) -> None: """Test that the advisor marks modules not ready for prod as alpha only.""" project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) self.user.features_enabled.alpha = True self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual( ['spontaneous-application', 'other-work-env'], [a.advice_id for a in project.advices]) self.assertEqual([False, True], [a.is_for_alpha_only for a in project.advices])
def test_redacted_email_address(self) -> None: """Test that we do not send any email if the email address is already redacted.""" project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, }, ]) self.user.profile.email = 'REDACTED' advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['spontaneous-application'], [a.advice_id for a in project.advices]) self.assertEqual(project_pb2.ADVICE_RECOMMENDED, project.advices[0].status)
def test_diagnostic_text_tutoiement_missing_translation( self, mock_logging): """Compute diagnostic text for tutoiement, but one tranlsation is missing.""" project = project_pb2.Project() self.user.profile.can_tutoie = True self.database.diagnostic_sentences.insert_many([ { 'sentenceTemplate': 'Vous êtes jeune.', 'order': 1, }, { 'sentenceTemplate': 'Nous allons vous aider.', 'order': 2, }, ]) self.database.translations.insert_one({ 'string': 'Vous êtes jeune.', 'fr_FR@tu': 'Tu es jeune.', }) advisor.maybe_diagnose(self.user, project, self.database) self.assertEqual('Tu es jeune.\n\nNous allons vous aider.', project.diagnostic.text) mock_logging.assert_called_once()
def _new_year_vars( user: user_pb2.User, *, now: datetime.datetime, **unused_kwargs: Any, ) -> dict[str, str]: """Compute all variables required for the New Year campaign.""" if now.month != 1 and not user.features_enabled.alpha: raise campaign.DoNotSend('Only send new-year email in January') project = next((p for p in user.projects), project_pb2.Project()) if project.passionate_level > project_pb2.PASSIONATING_JOB: goal = 'trouver un poste qui vous épanouira' elif project.kind == project_pb2.FIND_ANOTHER_JOB: goal = 'décrocher un nouveau poste' else: goal = 'décrocher votre prochain emploi' return campaign.get_default_coaching_email_vars(user) | { 'goal': goal, 'numberUsers': '250\u00A0000', 'lastYear': str(now.year - 1), 'year': str(now.year), }
def test_all_action_modules(self) -> None: """A user with all_modules enabled should get all actions.""" self.database.action_templates.insert_many([ { 'actionTemplateId': 'not-for-you', 'triggerScoringModel': 'constant(0)', }, { 'actionTemplateId': 'review-me', 'triggerScoringModel': 'constant(1)', 'tags': ['chrome-tool'], 'duration': 'FIFTEEN_TO_30_MIN', }, { 'actionTemplateId': 'finish-the-sprint', 'triggerScoringModel': 'constant(2)', }, ]) self.user.features_enabled.action_plan = features_pb2.ACTIVE self.user.features_enabled.all_modules = True project = project_pb2.Project( project_id='1234', target_job=job_pb2.Job( name='Steward/ Hôtesse', feminine_name='Hôtesse', masculine_name='Steward', ), ) advisor.maybe_advise(self.user, project, self.database) self.assertEqual( ['review-me', 'not-for-you', 'finish-the-sprint'], [a.action_id for a in project.actions])
def test_timeout_on_scoring(self, mock_warning, unused_mock_send_template): """Check that we don't wait scoring models for ever.""" patcher = mock.patch(advisor.scoring.__name__ + '.get_scoring_model') mock_get_scoring_model = patcher.start() self.addCleanup(patcher.stop) mock_get_scoring_model( ).score_and_explain.side_effect = lambda *unused_args: time.sleep(2) self.database.advice_modules.insert_one({ 'adviceId': 'crazy-advice', 'categories': ['first'], 'triggerScoringModel': 'very-long-to-respond', 'isReadyForProd': True, }) time_before_computing = time.time() advisor.compute_advices_for_project(self.user, project_pb2.Project(), self.database, scoring_timeout_seconds=0.01) time_after_computing = time.time() self.assertLess(time_after_computing - time_before_computing, 1) self.assertTrue(mock_warning.called) self.assertIn('Timeout while scoring', mock_warning.call_args[0][0]) self.assertEqual('very-long-to-respond', mock_warning.call_args[0][1])
def test_module_crashes(self, mock_logger, mock_scoring_models, mock_send_template): """Test that the advisor does not crash if one module does.""" mock_send_template().status_code = 200 mock_send_template.reset_mock() mock_scoring_models['constant(1)'] = mock.MagicMock(spec=['score']) mock_scoring_models['constant(1)'].score.return_value = 1 mock_scoring_models['crash-me'] = mock.MagicMock(spec=['score']) mock_scoring_models['crash-me'].score.side_effect = ValueError('ouch') project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'other-work-env', 'triggerScoringModel': 'crash-me', 'isReadyForProd': True, }, { 'adviceId': 'network', 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }, ]) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['network'], [a.advice_id for a in project.advices]) mock_send_template.assert_called_once() mock_logger.assert_called_once()
def test_recommend_all_modules(self) -> None: """Test that all advice are recommended when all_modules is true even if incompatible.""" project = project_pb2.Project() self.database.advice_modules.insert_many([ { 'adviceId': 'spontaneous-application', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, 'airtableId': 'abc', 'incompatibleAdviceIds': ['def'], }, { 'adviceId': 'other-work-env', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, 'airtableId': 'def', }, { 'adviceId': 'new-advice', 'categories': ['first'], 'triggerScoringModel': 'constant(0)', 'isReadyForProd': True, 'airtableId': 'def', 'incompatibleAdviceIds': ['abc'], }, ]) self.user.features_enabled.all_modules = True advisor.maybe_advise(self.user, project, self.database) self.assertEqual( ['spontaneous-application', 'other-work-env', 'new-advice'], [a.advice_id for a in project.advices])
def test_explained_advice(self, mock_scoring_models, mock_send_template): """Test that the advisor gives explanations for the advices.""" mock_send_template().status_code = 200 mock_send_template.reset_mock() mock_scoring_models['constant(1)'] = mock.MagicMock( spec=['score_and_explain']) mock_scoring_models['constant(1)'].score_and_explain.return_value = \ scoring.ExplainedScore(1, ['voilà pourquoi', 'explication genré%eFeminine']) project = project_pb2.Project() self.database.advice_modules.insert_one({ 'adviceId': 'network', 'categories': ['first'], 'triggerScoringModel': 'constant(1)', 'isReadyForProd': True, }) self.user.profile.gender = user_pb2.FEMININE advisor.maybe_advise(self.user, project, self.database) self.assertEqual(['network'], [a.advice_id for a in project.advices]) self.assertEqual(['voilà pourquoi', 'explication genrée'], project.advices[0].explanations) mock_send_template.assert_called_once()
def test_no_categories_if_already_categorized(self): """The advice_categories field does not get computed again.""" project = project_pb2.Project( advice_categories=[ project_pb2.AdviceCategory(category_id='very-old-category') ], advices=[ project_pb2.Advice(advice_id='spontaneous-application', status=project_pb2.ADVICE_RECOMMENDED, num_stars=3) ]) self.database.advice_modules.insert_one({ 'adviceId': 'spontaneous-application', 'categories': ['first', 'second'], 'triggerScoringModel': 'constant(2)', 'isReadyForProd': True, }) self.assertFalse( advisor.maybe_categorize_advice(self.user, project, self.database)) self.assertEqual(['very-old-category'], [c.category_id for c in project.advice_categories])
def test_diagnostic_text(self): """Compute a nice diagnostic text.""" project = project_pb2.Project(job_search_length_months=-1) project.target_job.feminine_name = 'Directrice technique' self.user.profile.year_of_birth = 2017 self.database.diagnostic_sentences.insert_many([ { 'sentenceTemplate': 'All good', 'order': 1, 'filters': ['not-for-young(25)'], }, { 'sentenceTemplate': 'Vous êtes jeune pour %aJobName.', 'order': 1, }, { 'sentenceTemplate': 'Nous allons vous aider.', 'filters': ['for-young(25)'], 'order': 2, }, { 'sentenceTemplate': 'Nous allons essayer de vous aider.', 'order': 2, }, ]) advisor.maybe_diagnose(self.user, project, self.database) self.assertEqual( 'Vous êtes jeune pour une directrice technique.\n\n' 'Nous allons vous aider.', project.diagnostic.text)
def test_no_categories_if_project_incomplete(self): """The advice_categorues field does not get populated when the project is marked as incomplete.""" project = project_pb2.Project(is_incomplete=True) self.assertFalse( advisor.maybe_categorize_advice(self.user, project, self.database))
def test_not_in_advisor(self): """User does not use the advisor yet.""" user_not_in_advisor = user_pb2.User( user_id=str(mongomock.ObjectId()), projects=[project_pb2.Project()], ) self.assertFalse(advisor.select_advice_for_email( user_not_in_advisor, user_pb2.TUESDAY, self.database))
def test_no_diagnostic_if_already_diagnosed(self): """The diagnostic does not get computed again.""" project = project_pb2.Project(is_incomplete=True, diagnostic=diagnostic_pb2.Diagnostic()) self.assertFalse( advisor.maybe_diagnose(self.user, project, self.database)) self.assertEqual('', str(project.diagnostic))
def test_no_advice_if_project_incomplete(self, mock_send_template): """Test that the advice do not get populated when the project is marked as incomplete.""" project = project_pb2.Project(is_incomplete=True) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(len(project.advices), 0) mock_send_template.assert_not_called()
def test_advice_spontaneous_application_extra_data(self, mock_get_lbb_companies): """Test that the advisor computes extra data for the "Spontaneous Application" advice.""" project = project_pb2.Project( target_job=job_pb2.Job(job_group=job_pb2.JobGroup( rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.job_group_info.insert_one({ '_id': 'A1234', 'applicationModes': { 'R4Z92': { 'modes': [{ 'percentage': 36.38, 'mode': 'SPONTANEOUS_APPLICATION' }, { 'percentage': 29.46, 'mode': 'PERSONAL_OR_PROFESSIONAL_CONTACTS' }, { 'percentage': 18.38, 'mode': 'PLACEMENT_AGENCY' }, { 'percentage': 15.78, 'mode': 'UNDEFINED_APPLICATION_MODE' }], } }, }) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'categories': ['first'], 'triggerScoringModel': 'advice-spontaneous-application', 'extraDataFieldName': 'spontaneous_application_data', 'isReadyForProd': True, }) mock_get_lbb_companies.return_value = iter([ { 'name': 'EX NIHILO' }, { 'name': 'M.F.P MULTIMEDIA FRANCE PRODUCTIONS' }, ]) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual( ['EX NIHILO', 'M.F.P MULTIMEDIA FRANCE PRODUCTIONS'], [c.name for c in advice.spontaneous_application_data.companies])
def test_started_months_ago(self): """Test the search_started_months_ago function.""" project = project_pb2.Project() self.assertEqual(-1, campaign.job_search_started_months_ago(project)) project.job_search_started_at.FromDatetime(datetime.datetime.now() - datetime.timedelta(days=61)) self.assertEqual(2, campaign.job_search_started_months_ago(project))
def test_no_advice_if_project_incomplete(self) -> None: """Test that the advice do not get populated when the project is marked as incomplete.""" project = project_pb2.Project(is_incomplete=True) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(len(project.advices), 0) self.assertFalse(mailjetmock.get_all_sent_messages())
def test_missing_module(self): """Test that the advisor does not crash when a module is missing.""" project = project_pb2.Project(advices=[project_pb2.Advice( advice_id='does-not-exist', status=project_pb2.ADVICE_ACCEPTED)]) project_before = str(project) advisor.maybe_advise(self.user, project, self.database) self.assertEqual(project_before, str(project))
def test_advice_better_job_in_group_extra_data(self): """Test that the advisor computes extra data for the "Better Job in Group" advice.""" project = project_pb2.Project( target_job=job_pb2.Job( code_ogr='1234', job_group=job_pb2.JobGroup(rome_id='A1234')), mobility=geo_pb2.Location(city=geo_pb2.FrenchCity( departement_id='14')), job_search_length_months=7, weekly_applications_estimate=project_pb2.A_LOT, total_interview_count=1, ) self.database.job_group_info.insert_one({ '_id': 'A1234', 'jobs': [ { 'codeOgr': '1234', 'name': 'Pilote' }, { 'codeOgr': '5678', 'name': 'Pompier' }, { 'codeOgr': '9012', 'name': 'Facteur' }, ], 'requirements': { 'specificJobs': [ { 'codeOgr': '5678', 'percentSuggested': 55, }, { 'codeOgr': '1234', 'percentSuggested': 45, }, ], }, }) self.database.advice_modules.insert_one({ 'adviceId': 'my-advice', 'triggerScoringModel': 'advice-better-job-in-group', 'extraDataFieldName': 'better_job_in_group_data', 'isReadyForProd': True, }) advisor.clear_cache() advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'my-advice') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual('Pompier', advice.better_job_in_group_data.better_job.name) self.assertEqual(1, advice.better_job_in_group_data.num_better_jobs)
def test_get_deep_link_missing_advice(self) -> None: """Test the deep link.""" project = project_pb2.Project() project.project_id = '0' project.advices.add().advice_id = 'improve-interview' deep_link = campaign.get_deep_link_advice('my-user-id', project, 'improve-cv') self.assertFalse(deep_link)
def test_string_representation(self) -> None: """A scoring project can be represented as a meaningful string.""" user = user_pb2.User() user.profile.gender = user_pb2.MASCULINE user.features_enabled.alpha = True project = project_pb2.Project(title='Developpeur web a Lyon') project_str = str(scoring.ScoringProject(project, user, None)) self.assertIn(str(user.profile), project_str) self.assertIn(str(project), project_str) self.assertIn(str(user.features_enabled), project_str)
def test_advice_specific_to_job_override(self) -> None: """Test that the advisor overrides some advice data with the "Specific to Job" module.""" project = project_pb2.Project(target_job=job_pb2.Job( job_group=job_pb2.JobGroup(rome_id='D1102')), ) self.database.advice_modules.insert_one({ 'adviceId': 'custom-advice-id', 'categories': ['first'], 'triggerScoringModel': 'advice-specific-to-job', 'isReadyForProd': True, }) self.database.specific_to_job_advice.insert_one({ 'title': 'Présentez-vous au chef boulanger dès son arrivée tôt le matin', 'shortTitle': 'Astuces de boulanger', 'goal': 'impressionner le patron', 'diagnosticTopics': [ diagnostic_pb2.MARKET_DIAGNOSTIC, diagnostic_pb2.PROJECT_DIAGNOSTIC ], 'filters': ['for-job-group(D1102)', 'not-for-job(12006)'], 'cardText': 'Allez à la boulangerie la veille pour savoir à quelle ' 'heure arrive le chef boulanger.', 'expandedCardHeader': "Voilà ce qu'il faut faire", 'expandedCardItems': [ 'Se présenter aux boulangers entre 4h et 7h du matin.', 'Demander au vendeur / à la vendeuse à quelle heure arrive le chef le matin', 'Contacter les fournisseurs de farine locaux : ils connaissent ' 'tous les boulangers du coin et sauront où il y a des ' 'embauches.', ], }) advisor.maybe_advise(self.user, project, self.database) advice = next(a for a in project.advices if a.advice_id == 'custom-advice-id') self.assertEqual(project_pb2.ADVICE_RECOMMENDED, advice.status) self.assertEqual( 'Présentez-vous au chef boulanger dès son arrivée tôt le matin', advice.title) self.assertEqual("Voilà ce qu'il faut faire", advice.expanded_card_header) self.assertTrue(advice.card_text) self.assertTrue(advice.expanded_card_items) self.assertEqual('Astuces de boulanger', advice.short_title) self.assertEqual([ diagnostic_pb2.MARKET_DIAGNOSTIC, diagnostic_pb2.PROJECT_DIAGNOSTIC ], advice.diagnostic_topics) self.assertEqual('impressionner le patron', advice.goal)
def test_no_tips(self): """No tips.""" self.database.advice_modules.insert_one({ 'adviceId': 'has-no-tips', }) tips = advisor.select_tips_for_email( user_pb2.User(user_id=str(mongomock.ObjectId())), project_pb2.Project(), advisor_pb2.AdviceModule(advice_id='has-no-tips'), self.database) self.assertFalse(tips)