Example #1
0
def test_v1_can_handle_rtd(client):
    file_upload = FileUpload(filename='requirements.txt', data=RTD)
    deps = json.loads(client.POST('/v1', data={'file': file_upload}).body)
    assert len(deps[0]['deps']) == 90
    assert deps[0]['deps'][14] == dict(name='Django',
                                       version='1.8.3',
                                       license='BSD')
Example #2
0
def test_v1_can_be_hit(client):
    file_upload = FileUpload(filename='requirements.txt', data='Flask==0.11.1')
    deps = json.loads(client.POST('/v1', data={'file': file_upload}).body)
    assert len(deps) == 1
    assert len(deps[0]['deps']) == 6
    assert deps[0]['deps'][1] == dict(name='Flask',
                                      version='0.11.1',
                                      license='BSD')
Example #3
0
def test_v1_accepts_multiple_files(client):
    one = FileUpload(filename='requirements.txt', data='Flask==0.11.1')
    two = FileUpload(filename='requirements.txt', data='itsdangerous==0.24')

    class Kludge:
        def items(self):
            for f in (one, two):
                yield 'file', f

    deps = json.loads(client.POST('/v1', data=Kludge()).body)
    assert len(deps) == 2
    assert len(deps[0]['deps']) == 6
    assert deps[0]['deps'][1] == dict(name='Flask',
                                      version='0.11.1',
                                      license='BSD')
    assert len(deps[1]['deps']) == 1
    assert deps[1]['deps'][0] == dict(name='itsdangerous',
                                      version='0.24',
                                      license='UNKNOWN')
Example #4
0
 def test_edit_with_invalid_image_type_raises_error(self):
     team = self.make_team(slug='enterprise', is_approved=True)
     invalid_image_types = ['tiff', 'gif', 'bmp', 'svg']
     for i_type in invalid_image_types:
         data = {'image': FileUpload(IMAGE, 'logo.' + i_type)}
         response = self.client.PxST('/enterprise/edit/edit.json',
                                     data=data,
                                     auth_as='picard')
         assert response.code == 400
         assert "Please upload a PNG or JPG image." in response.body
         assert team.load_image('original') == None
Example #5
0
 def test_edit_accepts_jpeg_and_png(self):
     team = self.make_team(slug='enterprise', is_approved=True)
     image_types = ['png', 'jpg', 'jpeg']
     for i_type in image_types:
         team.save_image(original='', large='', small='', image_type='image/png')
         data = {'image': FileUpload(IMAGE, 'logo.'+i_type)}
         response = self.client.POST( '/enterprise/edit/edit.json'
                                    , data=data
                                    , auth_as='picard'
                                     )
         assert response.code == 200
         assert team.load_image('original') == IMAGE
Example #6
0
    def test_edit_supports_partial_updates(self):
        self.make_team(slug='enterprise', is_approved=True)
        edit_data = {
            'product_or_service': 'We save galaxies.',
            'homepage': 'http://starwars-enterprise.com/',
            'image': FileUpload(IMAGE, 'logo.png'),
        }
        self.client.POST( '/enterprise/edit/edit.json'
                        , data=edit_data
                        , auth_as='picard'
                         )

        team = T('enterprise')
        assert team.name == 'The Enterprise'
        assert team.product_or_service == 'We save galaxies.'
        assert team.homepage == 'http://starwars-enterprise.com/'
        assert team.load_image('original') == IMAGE
Example #7
0
    def test_edit(self):
        self.make_team(slug='enterprise', is_approved=True)
        edit_data = {
            'name': 'Enterprise',
            'product_or_service': 'We save galaxies.',
            'homepage': 'http://starwars-enterprise.com/',
            'image': FileUpload(IMAGE, 'logo.png'),
        }
        data = json.loads(self.client.POST( '/enterprise/edit/edit.json'
                                          , data=edit_data
                                          , auth_as='picard'
                                           ).body)

        team = T('enterprise')
        assert data == team.to_dict()

        assert team.name == 'Enterprise'
        assert team.product_or_service == 'We save galaxies.'
        assert team.homepage == 'http://starwars-enterprise.com/'
        assert team.load_image('original') == IMAGE
Example #8
0
def test_v1_can_hit_back(client):
    file_upload = FileUpload(filename='requirements.txt',
                             data='Flasky garbage')
    deps = json.loads(client.POST('/v1', data={'file': file_upload}).body)
    assert len(deps) == 1
    assert deps[0]['error'].startswith('Traceback')

def bodified(raw):
    """Convert a plain dict into a cgi.FieldStorage per Aspen.
    """
    encoded = encode_multipart(BOUNDARY, raw)
    headers = BaseHeaders({b'Content-Type': b'multipart/form-data; boundary=' + BOUNDARY})
    return aspen.body_parsers.formdata(encoded, headers)


GOOD = { 'amount': '1000'
       , 'payment_method_nonce': 'fake-valid-nonce'
       , 'name': 'Alice Liddell'
       , 'email_address': '*****@*****.**'
       , 'on_mailing_list': 'yes'
       , 'promotion_logo': FileUpload(ORIGINAL, 'logo.png')
       , 'promotion_name': 'Wonderland'
       , 'promotion_url': 'http://www.example.com/'
       , 'promotion_twitter': 'thebestbutter'
       , 'promotion_message': 'Love me! Love me! Say that you love me!'
        }
BAD = { 'amount': '1,000'
      , 'payment_method_nonce': 'deadbeef' * 5
      , 'name': 'Alice Liddell' * 20
      , 'email_address': 'alice' * 100 + '@example.com'
      , 'on_mailing_list': 'cheese'
      , 'promotion_logo': FileUpload(ORIGINAL, 'logo.gif')
      , 'promotion_name': 'Wonderland' * 100
      , 'promotion_url': 'http://www.example.com/' + 'cheese' * 100
      , 'promotion_twitter': 'thebestbutter' * 10
      , 'promotion_message': 'Love me!' * 50
Example #10
0
class TestTeams(Harness):

    valid_data = {
        'name': 'Gratiteam',
        'product_or_service': 'We make widgets.',
        'homepage': 'http://gratipay.com/',
        'onboarding_url': 'http://inside.gratipay.com/',
        'todo_url': 'https://github.com/gratipay',
        'agree_public': 'true',
        'agree_payroll': 'true',
        'agree_terms': 'true',
        'image': FileUpload(IMAGE, 'logo.png'),
    }

    def post_new(self, data, auth_as='alice', expected=200):
        r = self.client.POST('/teams/create.json',
                             data=data,
                             auth_as=auth_as,
                             raise_immediately=False)
        assert r.code == expected
        return r

    def test_harness_can_make_a_team(self):
        team = self.make_team()
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    def test_can_construct_from_slug(self):
        self.make_team()
        team = Team.from_slug('TheEnterprise')
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    def test_can_construct_from_id(self):
        team = Team.from_id(self.make_team().id)
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    @mock.patch('gratipay.models.team.Team.create_github_review_issue')
    def test_can_create_new_team(self, cgri):
        cgri.return_value = REVIEW_URL
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='',
                              last_paypal_result='')
        r = self.post_new(dict(self.valid_data))
        team = self.db.one("SELECT * FROM teams")
        assert team
        assert team.owner == 'alice'
        assert json.loads(r.body)['review_url'] == team.review_url

    def test_all_fields_persist(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='',
                              last_paypal_result='')
        self.post_new(dict(self.valid_data))
        team = Team.from_slug('gratiteam')
        assert team.name == 'Gratiteam'
        assert team.homepage == 'http://gratipay.com/'
        assert team.product_or_service == 'We make widgets.'
        fallback = 'https://github.com/gratipay/team-review/issues#error-401'
        assert team.review_url in (REVIEW_URL, fallback)

    def test_casing_of_urls_survives(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='',
                              last_paypal_result='')
        self.post_new(
            dict(self.valid_data,
                 homepage='Http://gratipay.com/',
                 onboarding_url='http://INSIDE.GRATipay.com/',
                 todo_url='hTTPS://github.com/GRATIPAY'))
        team = Team.from_slug('gratiteam')
        assert team.homepage == 'Http://gratipay.com/'
        assert team.onboarding_url == 'http://INSIDE.GRATipay.com/'
        assert team.todo_url == 'hTTPS://github.com/GRATIPAY'

    def test_casing_of_slug_survives(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='',
                              last_paypal_result='')
        data = dict(self.valid_data)
        data['name'] = 'GratiTeam'
        self.post_new(dict(data))
        team = Team.from_slug('GratiTeam')
        assert team is not None
        assert team.slug_lower == 'gratiteam'

    def test_401_for_anon_creating_new_team(self):
        self.post_new(self.valid_data, auth_as=None, expected=401)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0

    def test_error_message_for_no_valid_email(self):
        self.make_participant('alice', claimed_time='now')
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "You must have a verified email address to apply for a new team." in r.body

    def test_error_message_for_no_payout_route(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**')
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "You must attach a PayPal account to apply for a new team." in r.body

    def test_error_message_for_public_review(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**',
                              last_paypal_result='')
        data = dict(self.valid_data)
        del data['agree_public']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, you must agree to have your application publicly reviewed." in r.body

    def test_error_message_for_payroll(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**',
                              last_paypal_result='')
        data = dict(self.valid_data)
        del data['agree_payroll']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, you must agree to be responsible for payroll." in r.body

    def test_error_message_for_terms(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**',
                              last_paypal_result='')
        data = dict(self.valid_data)
        del data['agree_terms']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, you must agree to the terms of service." in r.body

    def test_error_message_for_missing_fields(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**',
                              last_paypal_result='')
        data = dict(self.valid_data)
        del data['name']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please fill out the 'Team Name' field." in r.body

    def test_error_message_for_bad_url(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**',
                              last_paypal_result='')

        r = self.post_new(dict(self.valid_data, homepage='foo'), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please enter an http[s]:// URL for the 'Homepage' field." in r.body

        r = self.post_new(dict(self.valid_data, onboarding_url='foo'),
                          expected=400)
        assert "an http[s]:// URL for the 'Self-onboarding Documentation URL' field." in r.body

        r = self.post_new(dict(self.valid_data, todo_url='foo'), expected=400)
        assert "Please enter an http[s]:// URL for the 'To-do URL' field." in r.body

    def test_error_message_for_invalid_team_name(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**',
                              last_paypal_result='')
        data = dict(self.valid_data)
        data['name'] = '~Invalid:Name;'
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, your team name is invalid." in r.body

    def test_error_message_for_slug_collision(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**',
                              last_paypal_result='')
        self.post_new(dict(self.valid_data))
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 1
        assert "Sorry, there is already a team using 'Gratiteam'." in r.body

    def test_approved_team_shows_up_on_homepage(self):
        self.make_team(is_approved=True)
        assert 'The Enterprise' in self.client.GET("/").body

    def test_unreviewed_team_shows_up_on_homepage(self):
        self.make_team(is_approved=None)
        assert 'The Enterprise' in self.client.GET("/").body

    def test_rejected_team_shows_up_on_homepage(self):
        self.make_team(is_approved=False)
        assert 'The Enterprise' in self.client.GET("/").body

    def test_stripping_required_inputs(self):
        self.make_participant('alice',
                              claimed_time='now',
                              email_address='*****@*****.**',
                              last_paypal_result='')
        data = dict(self.valid_data)
        data['name'] = "     "
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please fill out the 'Team Name' field." in r.body

    # Migrate Tips
    # ============

    def test_migrate_tips_to_payment_instructions(self):
        alice = self.make_participant('alice', claimed_time='now')
        bob = self.make_participant('bob', claimed_time='now')
        self.make_participant('old_team')
        self.make_tip(alice, 'old_team', '1.00')
        self.make_tip(bob, 'old_team', '2.00')
        new_team = self.make_team('new_team', owner='old_team')

        ntips = new_team.migrate_tips()
        assert ntips == 2

        payment_instructions = self.db.all(
            "SELECT * FROM payment_instructions ORDER BY participant ASC")
        assert len(payment_instructions) == 2
        assert payment_instructions[0].participant == 'alice'
        assert payment_instructions[0].team == 'new_team'
        assert payment_instructions[0].amount == 1
        assert payment_instructions[1].participant == 'bob'
        assert payment_instructions[1].team == 'new_team'
        assert payment_instructions[1].amount == 2

    def test_migrate_tips_only_runs_once(self):
        alice = self.make_participant('alice', claimed_time='now')
        self.make_participant('old_team')
        self.make_tip(alice, 'old_team', '1.00')
        new_team = self.make_team('new_team', owner='old_team')

        new_team.migrate_tips()

        with pytest.raises(AlreadyMigrated):
            new_team.migrate_tips()

        payment_instructions = self.db.all(
            "SELECT * FROM payment_instructions")
        assert len(payment_instructions) == 1

    def test_migrate_tips_checks_for_multiple_teams(self):
        alice = self.make_participant('alice', claimed_time='now')
        self.make_participant('old_team')
        self.make_tip(alice, 'old_team', '1.00')
        new_team = self.make_team('new_team', owner='old_team')
        new_team.migrate_tips()

        newer_team = self.make_team('newer_team', owner='old_team')

        with pytest.raises(AlreadyMigrated):
            newer_team.migrate_tips()

        payment_instructions = self.db.all(
            "SELECT * FROM payment_instructions")
        assert len(payment_instructions) == 1

    # Dues, Upcoming Payment
    # ======================

    def test_get_dues(self):
        alice = self.make_participant('alice',
                                      claimed_time='now',
                                      last_bill_result='')
        bob = self.make_participant('bob',
                                    claimed_time='now',
                                    last_bill_result='Fail!')
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '3.00')  # Funded
        bob.set_payment_instruction(team, '5.00')  # Unfunded

        # Simulate dues
        self.db.run("UPDATE payment_instructions SET due = amount")

        assert team.get_dues() == (3, 5)

    def test_upcoming_payment(self):
        alice = self.make_participant('alice',
                                      claimed_time='now',
                                      last_bill_result='')
        bob = self.make_participant('bob',
                                    claimed_time='now',
                                    last_bill_result='')
        carl = self.make_participant('carl',
                                     claimed_time='now',
                                     last_bill_result='Fail!')
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '5.00')  # Funded
        bob.set_payment_instruction(
            team, '3.00')  # Funded, but won't hit minimum charge
        carl.set_payment_instruction(team, '10.00')  # Unfunded

        # Simulate dues
        self.db.run("UPDATE payment_instructions SET due = amount")

        assert team.get_upcoming_payment() == 10  # 2 * Alice's $5

    # Cached Values
    # =============

    def test_receiving_only_includes_funded_payment_instructions(self):
        alice = self.make_participant('alice',
                                      claimed_time='now',
                                      last_bill_result='')
        bob = self.make_participant('bob',
                                    claimed_time='now',
                                    last_bill_result="Fail!")
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(
            team, '3.00')  # The only funded payment instruction
        bob.set_payment_instruction(team, '5.00')

        assert team.receiving == Decimal('3.00')
        assert team.nreceiving_from == 1

        funded_payment_instruction = self.db.one(
            "SELECT * FROM payment_instructions "
            "WHERE is_funded ORDER BY id")
        assert funded_payment_instruction.participant == alice.username

    def test_receiving_only_includes_latest_payment_instructions(self):
        alice = self.make_participant('alice',
                                      claimed_time='now',
                                      last_bill_result='')
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '5.00')
        alice.set_payment_instruction(team, '3.00')

        assert team.receiving == Decimal('3.00')
        assert team.nreceiving_from == 1

    # Images
    # ======

    def test_save_image_saves_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        media_type = self.db.one('SELECT image_type FROM teams WHERE id=%s',
                                 (team.id, ))
        assert media_type == 'image/png'

    def test_save_image_records_the_event(self):
        team = self.make_team()
        oids = team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        event = self.db.one('SELECT * FROM events')
        assert event.payload == {
            'action': 'upsert_image',
            'original': oids['original'],
            'large': oids['large'],
            'small': oids['small'],
            'id': team.id
        }

    def test_load_image_loads_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        image = team.load_image('large')  # buffer
        assert str(image) == IMAGE

    def test_image_endpoint_serves_an_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        image = self.client.GET('/TheEnterprise/image').body  # buffer
        assert str(image) == IMAGE

    # Update
    # ======

    def test_update_works(self):
        team = self.make_team(slug='enterprise')
        update_data = {
            'name': 'Enterprise',
            'product_or_service': 'We save galaxies.',
            'homepage': 'http://starwars-enterprise.com/',
            'onboarding_url': 'http://starwars-enterprise.com/onboarding',
            'todo_url': 'http://starwars-enterprise.com/todos',
        }
        team.update(**update_data)
        team = Team.from_slug('enterprise')
        for field in update_data:
            assert getattr(team, field) == update_data[field]

    def test_can_only_update_allowed_fields(self):
        allowed_fields = set([
            'name', 'product_or_service', 'homepage', 'onboarding_url',
            'todo_url'
        ])

        team = self.make_team(slug='enterprise')

        fields = vars(team).keys()
        for field in fields:
            if field not in allowed_fields:
                with pytest.raises(AssertionError):
                    team.update(field='foo')

    def test_update_records_the_old_values_as_events(self):
        team = self.make_team(slug='enterprise', product_or_service='Product')
        team.update(name='Enterprise', product_or_service='We save galaxies.')
        event = self.db.one('SELECT * FROM events')
        assert event.payload == {
            'action': 'update',
            'id': team.id,
            'name': 'The Enterprise',
            'product_or_service': 'Product'
        }

    def test_update_updates_object_attributes(self):
        team = self.make_team(slug='enterprise')
        team.update(name='Enterprise', product_or_service='We save galaxies.')
        assert team.name == 'Enterprise'
        assert team.product_or_service == 'We save galaxies.'

    # slugize

    def test_slugize_slugizes(self):
        assert slugize('Foo') == 'Foo'

    def test_slugize_requires_a_letter(self):
        assert pytest.raises(InvalidTeamName, slugize, '123')

    def test_slugize_accepts_letter_in_middle(self):
        assert slugize('1a23') == '1a23'

    def test_slugize_converts_comma_to_dash(self):
        assert slugize('foo,bar') == 'foo-bar'

    def test_slugize_converts_space_to_dash(self):
        assert slugize('foo bar') == 'foo-bar'

    def test_slugize_allows_underscore(self):
        assert slugize('foo_bar') == 'foo_bar'

    def test_slugize_allows_period(self):
        assert slugize('foo.bar') == 'foo.bar'

    def test_slugize_trims_whitespace(self):
        assert slugize('  Foo Bar  ') == 'Foo-Bar'

    def test_slugize_trims_dashes(self):
        assert slugize('--Foo Bar--') == 'Foo-Bar'

    def test_slugize_trims_replacement_dashes(self):
        assert slugize(',,Foo Bar,,') == 'Foo-Bar'

    def test_slugize_folds_dashes_together(self):
        assert slugize('1a----------------23') == '1a-23'
Example #11
0
class TestTeams(Harness):

    valid_data = {
        'name': 'Gratiteam',
        'product_or_service': 'We make widgets.',
        'homepage': 'http://gratipay.com/',
        'onboarding_url': 'http://inside.gratipay.com/',
        'todo_url': 'https://github.com/gratipay',
        'agree_public': 'true',
        'agree_payroll': 'true',
        'agree_terms': 'true',
        'image': FileUpload(IMAGE, 'logo.png'),
    }

    def post_new(self, data, auth_as='alice', expected=200):
        r =  self.client.POST( '/teams/create.json'
                             , data=data
                             , auth_as=auth_as
                             , raise_immediately=False
                              )
        assert r.code == expected
        return r

    def test_harness_can_make_a_team(self):
        team = self.make_team()
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    def test_can_construct_from_slug(self):
        self.make_team()
        team = Team.from_slug('TheEnterprise')
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    def test_can_construct_from_id(self):
        team = Team.from_id(self.make_team().id)
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    @mock.patch('gratipay.models.team.Team.create_github_review_issue')
    def test_can_create_new_team(self, cgri):
        cgri.return_value = REVIEW_URL
        self.make_participant('alice', claimed_time='now', email_address='', last_paypal_result='')
        r = self.post_new(dict(self.valid_data))
        team = self.db.one("SELECT * FROM teams")
        assert team
        assert team.owner == 'alice'
        assert json.loads(r.body)['review_url'] == team.review_url

    def test_all_fields_persist(self):
        self.make_participant('alice', claimed_time='now', email_address='', last_paypal_result='')
        self.post_new(dict(self.valid_data))
        team = Team.from_slug('gratiteam')
        assert team.name == 'Gratiteam'
        assert team.homepage == 'http://gratipay.com/'
        assert team.product_or_service == 'We make widgets.'
        fallback = 'https://github.com/gratipay/team-review/issues#error-401'
        assert team.review_url in (REVIEW_URL, fallback)

    def test_casing_of_urls_survives(self):
        self.make_participant('alice', claimed_time='now', email_address='', last_paypal_result='')
        self.post_new(dict( self.valid_data
                          , homepage='Http://gratipay.com/'
                          , onboarding_url='http://INSIDE.GRATipay.com/'
                          , todo_url='hTTPS://github.com/GRATIPAY'
                           ))
        team = Team.from_slug('gratiteam')
        assert team.homepage == 'Http://gratipay.com/'
        assert team.onboarding_url == 'http://INSIDE.GRATipay.com/'
        assert team.todo_url == 'hTTPS://github.com/GRATIPAY'

    def test_401_for_anon_creating_new_team(self):
        self.post_new(self.valid_data, auth_as=None, expected=401)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0

    def test_error_message_for_no_valid_email(self):
        self.make_participant('alice', claimed_time='now')
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "You must have a verified email address to apply for a new team." in r.body

    def test_error_message_for_no_payout_route(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**')
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "You must attach a PayPal account to apply for a new team." in r.body

    def test_error_message_for_public_review(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**', last_paypal_result='')
        data = dict(self.valid_data)
        del data['agree_public']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, you must agree to have your application publicly reviewed." in r.body

    def test_error_message_for_payroll(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**', last_paypal_result='')
        data = dict(self.valid_data)
        del data['agree_payroll']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, you must agree to be responsible for payroll." in r.body

    def test_error_message_for_terms(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**', last_paypal_result='')
        data = dict(self.valid_data)
        del data['agree_terms']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, you must agree to the terms of service." in r.body

    def test_error_message_for_missing_fields(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**', last_paypal_result='')
        data = dict(self.valid_data)
        del data['name']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please fill out the 'Team Name' field." in r.body

    def test_error_message_for_bad_url(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**', last_paypal_result='')

        r = self.post_new(dict(self.valid_data, homepage='foo'), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please enter an http[s]:// URL for the 'Homepage' field." in r.body

        r = self.post_new(dict(self.valid_data, onboarding_url='foo'), expected=400)
        assert "an http[s]:// URL for the 'Self-onboarding Documentation URL' field." in r.body

        r = self.post_new(dict(self.valid_data, todo_url='foo'), expected=400)
        assert "Please enter an http[s]:// URL for the 'To-do URL' field." in r.body

    def test_error_message_for_slug_collision(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**', last_paypal_result='')
        self.post_new(dict(self.valid_data))
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 1
        assert "Sorry, there is already a team using 'gratiteam'." in r.body

    def test_approved_team_shows_up_on_homepage(self):
        self.make_team(is_approved=True)
        assert 'The Enterprise' in self.client.GET("/").body

    def test_unreviewed_team_shows_up_on_homepage(self):
        self.make_team(is_approved=None)
        assert 'The Enterprise' in self.client.GET("/").body

    def test_rejected_team_shows_up_on_homepage(self):
        self.make_team(is_approved=False)
        assert 'The Enterprise' in self.client.GET("/").body

    def test_stripping_required_inputs(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**', last_paypal_result='')
        data = dict(self.valid_data)
        data['name'] = "     "
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please fill out the 'Team Name' field." in r.body

    def test_migrate_tips_to_payment_instructions(self):
        alice = self.make_participant('alice', claimed_time='now')
        bob = self.make_participant('bob', claimed_time='now')
        self.make_participant('old_team')
        self.make_tip(alice, 'old_team', '1.00')
        self.make_tip(bob, 'old_team', '2.00')
        new_team = self.make_team('new_team', owner='old_team')

        ntips = new_team.migrate_tips()
        assert ntips == 2

        payment_instructions = self.db.all("SELECT * FROM payment_instructions ORDER BY participant ASC")
        assert len(payment_instructions) == 2
        assert payment_instructions[0].participant == 'alice'
        assert payment_instructions[0].team == 'new_team'
        assert payment_instructions[0].amount == 1
        assert payment_instructions[1].participant == 'bob'
        assert payment_instructions[1].team == 'new_team'
        assert payment_instructions[1].amount == 2

    def test_migrate_tips_only_runs_once(self):
        alice = self.make_participant('alice', claimed_time='now')
        self.make_participant('old_team')
        self.make_tip(alice, 'old_team', '1.00')
        new_team = self.make_team('new_team', owner='old_team')

        new_team.migrate_tips()

        with pytest.raises(AlreadyMigrated):
            new_team.migrate_tips()

        payment_instructions = self.db.all("SELECT * FROM payment_instructions")
        assert len(payment_instructions) == 1

    def test_migrate_tips_checks_for_multiple_teams(self):
        alice = self.make_participant('alice', claimed_time='now')
        self.make_participant('old_team')
        self.make_tip(alice, 'old_team', '1.00')
        new_team = self.make_team('new_team', owner='old_team')
        new_team.migrate_tips()

        newer_team = self.make_team('newer_team', owner='old_team')

        with pytest.raises(AlreadyMigrated):
            newer_team.migrate_tips()

        payment_instructions = self.db.all("SELECT * FROM payment_instructions")
        assert len(payment_instructions) == 1


    # cached values - receiving, nreceiving_from

    def test_receiving_only_includes_funded_payment_instructions(self):
        alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
        bob = self.make_participant('bob', claimed_time='now', last_bill_result="Fail!")
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '3.00') # The only funded payment instruction
        bob.set_payment_instruction(team, '5.00')

        assert team.receiving == Decimal('3.00')
        assert team.nreceiving_from == 1

        funded_payment_instruction = self.db.one("SELECT * FROM payment_instructions "
                                                 "WHERE is_funded ORDER BY id")
        assert funded_payment_instruction.participant == alice.username

    def test_receiving_only_includes_latest_payment_instructions(self):
        alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '5.00')
        alice.set_payment_instruction(team, '3.00')

        assert team.receiving == Decimal('3.00')
        assert team.nreceiving_from == 1


    # Images
    # ======

    def test_save_image_saves_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        media_type = self.db.one('SELECT image_type FROM teams WHERE id=%s', (team.id,))
        assert media_type == 'image/png'

    def test_save_image_records_the_event(self):
        team = self.make_team()
        oids = team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        event = self.db.one('SELECT * FROM events')
        assert event.payload == { 'action': 'upsert_image'
                                , 'original': oids['original']
                                , 'large': oids['large']
                                , 'small': oids['small']
                                , 'id': team.id
                                 }

    def test_load_image_loads_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        image = team.load_image('large')  # buffer
        assert str(image) == IMAGE

    def test_image_endpoint_serves_an_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        image = self.client.GET('/TheEnterprise/image').body  # buffer
        assert str(image) == IMAGE
Example #12
0
class TestTeams(QueuedEmailHarness):

    valid_data = {
        'name': 'Gratiteam',
        'product_or_service': 'We make widgets.',
        'homepage': 'http://gratipay.com/',
        'onboarding_url': 'http://inside.gratipay.com/',
        'agree_public': 'true',
        'agree_payroll': 'true',
        'agree_terms': 'true',
        'image': FileUpload(IMAGE, 'logo.png'),
    }

    def post_new(self, data, auth_as='alice', expected=200):
        r =  self.client.POST( '/teams/create.json'
                             , data=data
                             , auth_as=auth_as
                             , raise_immediately=False
                              )
        assert r.code == expected
        return r

    def test_harness_can_make_a_team(self):
        team = self.make_team()
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    def test_can_construct_from_slug(self):
        self.make_team()
        team = Team.from_slug('TheEnterprise')
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    def test_can_construct_from_id(self):
        team = Team.from_id(self.make_team().id)
        assert team.name == 'The Enterprise'
        assert team.owner == 'picard'

    def make_alice(self):
        self.make_participant( 'alice'
                             , claimed_time='now'
                             , email_address='*****@*****.**'
                             , last_paypal_result=''
                              )

    def test_can_create_new_team(self):
        self.make_alice()
        r = self.post_new(dict(self.valid_data))
        team = self.db.one("SELECT * FROM teams")
        assert team
        assert team.owner == 'alice'
        assert json.loads(r.body)['review_url'] == team.review_url

    def test_all_fields_persist(self):
        self.make_alice()
        self.post_new(dict(self.valid_data))
        team = T('gratiteam')
        assert team.name == 'Gratiteam'
        assert team.homepage == 'http://gratipay.com/'
        assert team.product_or_service == 'We make widgets.'
        assert team.review_url == 'some-github-issue'

    def test_casing_of_urls_survives(self):
        self.make_alice()
        self.post_new(dict( self.valid_data
                          , homepage='Http://gratipay.com/'
                           ))
        team = T('gratiteam')
        assert team.homepage == 'Http://gratipay.com/'

    def test_casing_of_slug_survives(self):
        self.make_alice()
        data = dict(self.valid_data)
        data['name'] = 'GratiTeam'
        self.post_new(dict(data))
        team = T('GratiTeam')
        assert team is not None
        assert team.slug_lower == 'gratiteam'

    def test_application_email_sent_to_owner(self):
        self.make_alice()
        self.post_new(dict(self.valid_data))
        last_email = self.get_last_email()
        self.app.email_queue.flush()
        assert last_email['to'] == 'alice <*****@*****.**>'
        expected = "Thanks for applying to use Gratipay!"
        assert expected in last_email['body_text']

    def test_401_for_anon_creating_new_team(self):
        self.post_new(self.valid_data, auth_as=None, expected=401)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0

    def test_error_message_for_no_valid_email(self):
        self.make_participant('alice', claimed_time='now')
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "You must have a verified email address to apply for a new team." in r.body

    def test_error_message_for_no_payout_route(self):
        self.make_participant('alice', claimed_time='now', email_address='*****@*****.**')
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "You must attach a PayPal account to apply for a new team." in r.body

    def test_error_message_for_public_review(self):
        self.make_alice()
        data = dict(self.valid_data)
        del data['agree_public']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, you must agree to have your application publicly reviewed." in r.body

    def test_error_message_for_terms(self):
        self.make_alice()
        data = dict(self.valid_data)
        del data['agree_terms']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, you must agree to the terms of service." in r.body

    def test_error_message_for_missing_fields(self):
        self.make_alice()
        data = dict(self.valid_data)
        del data['name']
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please fill out the 'Team Name' field." in r.body

    def test_error_message_for_bad_url(self):
        self.make_alice()
        r = self.post_new(dict(self.valid_data, homepage='foo'), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please enter an http[s]:// URL for the 'Homepage' field." in r.body

    def test_error_message_for_invalid_team_name(self):
        self.make_alice()
        data = dict(self.valid_data)
        data['name'] = '~Invalid:Name;'
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Sorry, your team name is invalid." in r.body

    def test_error_message_for_slug_collision(self):
        self.make_alice()
        self.post_new(dict(self.valid_data))
        r = self.post_new(dict(self.valid_data), expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 1
        assert "Sorry, there is already a team using 'Gratiteam'." in r.body

    def test_stripping_required_inputs(self):
        self.make_alice()
        data = dict(self.valid_data)
        data['name'] = "     "
        r = self.post_new(data, expected=400)
        assert self.db.one("SELECT COUNT(*) FROM teams") == 0
        assert "Please fill out the 'Team Name' field." in r.body

    def test_receiving_page_basically_works(self):
        team = self.make_team(is_approved=True)
        alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
        alice.set_payment_instruction(team, '3.00')
        body = self.client.GET('/TheEnterprise/receiving/', auth_as='picard').body
        assert '100.0%' in body


    # Dues, Upcoming Payment
    # ======================

    def test_get_dues(self):
        alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
        bob = self.make_participant('bob', claimed_time='now', last_bill_result='Fail!')
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '3.00') # Funded
        bob.set_payment_instruction(team, '5.00') # Unfunded

        # Simulate dues
        self.db.run("UPDATE payment_instructions SET due = amount")

        assert team.get_dues() == (3, 5)


    def test_upcoming_payment(self):
        alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
        bob = self.make_participant('bob', claimed_time='now', last_bill_result='')
        carl = self.make_participant('carl', claimed_time='now', last_bill_result='Fail!')
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '5.00') # Funded
        bob.set_payment_instruction(team, '3.00') # Funded, but won't hit minimum charge
        carl.set_payment_instruction(team, '10.00') # Unfunded

        # Simulate dues
        self.db.run("UPDATE payment_instructions SET due = amount")

        assert team.get_upcoming_payment() == 10 # 2 * Alice's $5

    # Cached Values
    # =============

    def test_receiving_only_includes_funded_payment_instructions(self):
        alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
        bob = self.make_participant('bob', claimed_time='now', last_bill_result="Fail!")
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '3.00') # The only funded payment instruction
        bob.set_payment_instruction(team, '5.00')

        assert team.receiving == D('3.00')
        assert team.nreceiving_from == 1

        funded_payment_instruction = self.db.one("SELECT * FROM payment_instructions "
                                                 "WHERE is_funded ORDER BY id")
        assert funded_payment_instruction.participant_id == alice.id

    def test_receiving_only_includes_latest_payment_instructions(self):
        alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
        team = self.make_team(is_approved=True)

        alice.set_payment_instruction(team, '5.00')
        alice.set_payment_instruction(team, '3.00')

        assert team.receiving == D('3.00')
        assert team.nreceiving_from == 1


    # Images
    # ======

    def test_save_image_saves_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        media_type = self.db.one('SELECT image_type FROM teams WHERE id=%s', (team.id,))
        assert media_type == 'image/png'

    def test_save_image_records_the_event(self):
        team = self.make_team()
        oids = team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        event = self.db.all('SELECT * FROM events ORDER BY ts DESC')[0]
        assert event.type == 'team'
        assert event.payload == { 'action': 'upsert_image'
                                , 'original': oids['original']
                                , 'large': oids['large']
                                , 'small': oids['small']
                                , 'id': team.id
                                 }

    def test_load_image_loads_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        image = team.load_image('large')  # buffer
        assert str(image) == IMAGE

    def test_image_endpoint_serves_an_image(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        image = self.client.GET('/TheEnterprise/image').body  # buffer
        assert str(image) == IMAGE

    def test_get_image_url_gets_image_url(self):
        team = self.make_team()
        team.save_image(IMAGE, IMAGE, IMAGE, 'image/png')
        assert team.get_image_url('small') == '/TheEnterprise/image?size=small'


    # Update
    # ======

    def test_update_works(self):
        team = self.make_team(slug='enterprise')
        update_data = {
            'name': 'Enterprise',
            'product_or_service': 'We save galaxies.',
            'homepage': 'http://starwars-enterprise.com/',
            'onboarding_url': 'http://starwars-enterprise.com/onboarding',
        }
        team.update(**update_data)
        team = T('enterprise')
        for field in update_data:
            assert getattr(team, field) == update_data[field]

    def test_can_only_update_allowed_fields(self):
        allowed_fields = set(['name', 'product_or_service', 'homepage',
                              'onboarding_url',])

        team = self.make_team(slug='enterprise')

        fields = vars(team).keys()
        for field in fields:
            if field not in allowed_fields:
                with pytest.raises(AssertionError):
                    team.update(field='foo')

    def test_homepage_not_allowed_for_package(self):
        alice = self.make_participant('alice', claimed_time='now')
        package = self.make_package(name='enterprise')
        with self.db.get_cursor() as c:
            team = package.get_or_create_linked_team(c, alice)
        pytest.raises(AssertionError, team.update, homepage='foo')

    def test_update_records_the_old_values_as_events(self):
        team = self.make_team(slug='enterprise', product_or_service='Product')
        team.update(name='Enterprise', product_or_service='We save galaxies.')
        event = self.db.all('SELECT * FROM events ORDER BY ts DESC')[0]
        assert event.payload == { 'action': 'update'
                                , 'id': team.id
                                , 'name': 'The Enterprise'
                                , 'product_or_service': 'Product'
                                 }

    def test_update_updates_object_attributes(self):
        team = self.make_team(slug='enterprise')
        team.update(name='Enterprise', product_or_service='We save galaxies.')
        assert team.name == 'Enterprise'
        assert team.product_or_service == 'We save galaxies.'


    # slugize

    def test_slugize_slugizes(self):
        assert slugize('Foo') == 'Foo'

    def test_slugize_requires_a_letter(self):
        assert pytest.raises(InvalidTeamName, slugize, '123')

    def test_slugize_accepts_letter_in_middle(self):
        assert slugize('1a23') == '1a23'

    def test_slugize_converts_comma_to_dash(self):
        assert slugize('foo,bar') == 'foo-bar'

    def test_slugize_converts_space_to_dash(self):
        assert slugize('foo bar') == 'foo-bar'

    def test_slugize_allows_underscore(self):
        assert slugize('foo_bar') == 'foo_bar'

    def test_slugize_allows_period(self):
        assert slugize('foo.bar') == 'foo.bar'

    def test_slugize_trims_whitespace(self):
        assert slugize('  Foo Bar  ') == 'Foo-Bar'

    def test_slugize_trims_dashes(self):
        assert slugize('--Foo Bar--') == 'Foo-Bar'

    def test_slugize_trims_replacement_dashes(self):
        assert slugize(',,Foo Bar,,') == 'Foo-Bar'

    def test_slugize_folds_dashes_together(self):
        assert slugize('1a----------------23') == '1a-23'

    def test_slugize_disallows_slashes(self):
        self.assertRaises(InvalidTeamName, slugize, 'abc/def')

    def test_slugize_disallows_questions(self):
        self.assertRaises(InvalidTeamName, slugize, 'abc?def')

    def test_slugize_disallows_backslashes(self):
        self.assertRaises(InvalidTeamName, slugize, 'abc\def')