import json from grant.utils import totp_2fa from grant.task.jobs import MilestoneDeadline from datetime import datetime, timedelta from grant.task.models import Task, db from grant.task.jobs import PruneDraft from grant.milestone.models import Milestone from grant.proposal.models import Proposal, ProposalUpdate from grant.utils.enums import ProposalStatus, ProposalStage, Category from ..config import BaseProposalCreatorConfig from ..test_data import mock_blockchain_api_requests from mock import patch, Mock test_update = { "title": "Update Title", "content": "Update content." } milestones_data = [ { "title": "All the money straightaway", "content": "cool stuff with it", "days_estimated": 30, "payout_percent": "100", "immediate_payout": False } ] class TestTaskAPI(BaseProposalCreatorConfig): def p(self, path, data): return self.app.post(path, data=json.dumps(data), content_type="application/json") def login_admin(self): # set admin self.user.set_admin(True) db.session.commit() # login r = self.p("/api/v1/admin/login", { "username": self.user.email_address, "password": self.user_password }) self.assert200(r) # 2fa on the natch r = self.app.get("/api/v1/admin/2fa") self.assert200(r) # ... init r = self.app.get("/api/v1/admin/2fa/init") self.assert200(r) codes = r.json['backupCodes'] secret = r.json['totpSecret'] uri = r.json['totpUri'] # ... enable/verify r = self.p("/api/v1/admin/2fa/enable", { "backupCodes": codes, "totpSecret": secret, "verifyCode": totp_2fa.current_totp(secret) }) self.assert200(r) return r def test_proposal_reminder_task_is_created(self): tasks = Task.query.filter(Task.execute_after <= datetime.now()).filter_by(completed=False).all() self.assertEqual(tasks, []) self.make_proposal_reminder_task() tasks = Task.query.filter(Task.execute_after <= datetime.now()).filter_by(completed=False).all() self.assertEqual(len(tasks), 1) def test_proposal_reminder_task_is_marked_completed_after_call(self): self.make_proposal_reminder_task() tasks = Task.query.filter(Task.execute_after <= datetime.now()).filter_by(completed=False).all() self.assertEqual(len(tasks), 1) self.app.get("/api/v1/task") tasks = Task.query.filter(Task.execute_after <= datetime.now()).filter_by(completed=False).all() self.assertEqual(tasks, []) @patch('grant.task.views.datetime') def test_proposal_pruning(self, mock_datetime): self.login_default_user() resp = self.app.post( "/api/v1/proposals/drafts", ) proposal_id = resp.json['proposalId'] # make sure proposal was created proposal = Proposal.query.get(proposal_id) self.assertIsNotNone(proposal) # make sure the task was created self.assertStatus(resp, 201) tasks = Task.query.all() self.assertEqual(len(tasks), 1) task = tasks[0] self.assertEqual(resp.json['proposalId'], task.blob['proposal_id']) self.assertFalse(task.completed) # mock time so task will run when called after_time = datetime.now() + timedelta(seconds=PruneDraft.PRUNE_TIME + 100) mock_datetime.now = Mock(return_value=after_time) # run task resp = self.app.get("/api/v1/task") self.assert200(resp) # make sure task ran successfully tasks = Task.query.all() self.assertEqual(len(tasks), 1) task = tasks[0] self.assertTrue(task.completed) proposal = Proposal.query.get(proposal_id) self.assertIsNone(proposal) def test_proposal_pruning_noops(self): # ensure all proposal noop states work as expected def status(p): p.status = ProposalStatus.LIVE def title(p): p.title = 'title' def brief(p): p.brief = 'brief' def content(p): p.content = 'content' def category(p): p.category = Category.DEV_TOOL def target(p): p.target = '100' def payout_address(p): p.payout_address = 'address' def milestones(p): Milestone.make(milestones_data, p) modifiers = [ status, title, brief, content, category, target, payout_address, milestones ] for modifier in modifiers: proposal = Proposal.create(status=ProposalStatus.DRAFT) proposal_id = proposal.id modifier(proposal) db.session.add(proposal) db.session.commit() blob = { "proposal_id": proposal_id, } task = Task( job_type=PruneDraft.JOB_TYPE, blob=blob, execute_after=datetime.now() ) PruneDraft.process_task(task) proposal = Proposal.query.get(proposal_id) self.assertIsNotNone(proposal) @patch('grant.task.jobs.send_email') @patch('grant.task.views.datetime') def test_milestone_deadline(self, mock_datetime, mock_send_email): tasks = Task.query.filter_by(completed=False).all() self.assertEqual(len(tasks), 0) self.proposal.arbiter.user = self.user db.session.add(self.proposal) # unset immediate_payout so task will be added for milestone in self.proposal.milestones: if milestone.immediate_payout: milestone.immediate_payout = False db.session.add(milestone) db.session.commit() self.login_admin() # proposal needs to be DISCUSSION self.proposal.status = ProposalStatus.DISCUSSION # approve proposal with funding resp = self.app.put( "/api/v1/admin/proposals/{}/accept".format(self.proposal.id), data=json.dumps({"isAccepted": True, "withFunding": True}) ) self.assert200(resp) tasks = Task.query.filter_by(completed=False).all() self.assertEqual(len(tasks), 1) # fast forward the clock so task will run after_time = datetime.now() + timedelta(days=365) mock_datetime.now = Mock(return_value=after_time) # run task resp = self.app.get("/api/v1/task") self.assert200(resp) # make sure task ran tasks = Task.query.filter_by(completed=False).all() self.assertEqual(len(tasks), 0) mock_send_email.assert_called() @patch('grant.task.jobs.send_email') def test_milestone_deadline_update_posted(self, mock_send_email): tasks = Task.query.all() self.assertEqual(len(tasks), 0) # set date_estimated on milestone to be in the past milestone = self.proposal.milestones[0] milestone.date_estimated = datetime.now() - timedelta(hours=1) db.session.add(milestone) db.session.commit() # make task ms_deadline = MilestoneDeadline(self.proposal, milestone) ms_deadline.make_task() # check make task tasks = Task.query.all() self.assertEqual(len(tasks), 1) # login and post proposal update self.login_default_user() resp = self.app.post( "/api/v1/proposals/{}/updates".format(self.proposal.id), data=json.dumps(test_update), content_type='application/json' ) self.assertStatus(resp, 201) # run task resp = self.app.get("/api/v1/task") self.assert200(resp) # make sure task ran and did NOT send out an email tasks = Task.query.filter_by(completed=False).all() self.assertEqual(len(tasks), 0) mock_send_email.assert_not_called() @patch('grant.task.jobs.send_email') def test_milestone_deadline_noops(self, mock_send_email): # make sure all milestone deadline noop states work as expected def proposal_delete(p, m): db.session.delete(p) def proposal_status(p, m): p.status = ProposalStatus.DELETED db.session.add(p) def proposal_stage(p, m): p.stage = ProposalStage.CANCELED db.session.add(p) def milestone_delete(p, m): db.session.delete(m) def milestone_date_requested(p, m): m.date_requested = datetime.now() db.session.add(m) def update_posted(p, m): # login and post proposal update self.login_default_user() resp = self.app.post( "/api/v1/proposals/{}/updates".format(proposal.id), data=json.dumps(test_update), content_type='application/json' ) self.assertStatus(resp, 201) modifiers = [ proposal_delete, proposal_status, proposal_stage, milestone_delete, milestone_date_requested, update_posted ] for modifier in modifiers: # make proposal and milestone proposal = Proposal.create(status=ProposalStatus.LIVE) proposal.arbiter.user = self.other_user proposal.team.append(self.user) proposal_id = proposal.id Milestone.make(milestones_data, proposal) db.session.add(proposal) db.session.commit() # grab update count for blob update_count = len(ProposalUpdate.query.filter_by(proposal_id=proposal_id).all()) # run modifications to trigger noop proposal = Proposal.query.get(proposal_id) milestone = proposal.milestones[0] milestone_id = milestone.id modifier(proposal, milestone) db.session.commit() # make task blob = { "proposal_id": proposal_id, "milestone_id": milestone_id, "update_count": update_count } task = Task( job_type=MilestoneDeadline.JOB_TYPE, blob=blob, execute_after=datetime.now() ) # run task MilestoneDeadline.process_task(task) # check to make sure noop occurred mock_send_email.assert_not_called()