2022-03-12 15:19:14 -05:00
|
|
|
import os
|
|
|
|
import subprocess
|
|
|
|
import time
|
2021-08-18 15:39:14 -04:00
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
import ldap3
|
2021-08-13 20:11:56 -04:00
|
|
|
import pytest
|
2021-12-11 16:30:18 -05:00
|
|
|
import datetime
|
2021-08-13 20:11:56 -04:00
|
|
|
|
2021-12-11 16:30:18 -05:00
|
|
|
import ceod.utils
|
|
|
|
import ceo_common.utils
|
|
|
|
from ceo_common.model import Term
|
2021-08-18 15:39:14 -04:00
|
|
|
|
2021-08-13 20:11:56 -04:00
|
|
|
|
2021-08-18 15:39:14 -04:00
|
|
|
def test_api_user_not_found(client):
|
2021-08-13 20:11:56 -04:00
|
|
|
status, data = client.get('/api/members/no_such_user')
|
|
|
|
assert status == 404
|
|
|
|
|
|
|
|
|
2021-08-19 12:58:59 -04:00
|
|
|
@pytest.fixture(scope='module')
|
2021-12-11 16:30:18 -05:00
|
|
|
def create_user_resp(client, mocks_for_create_user_module, mock_mail_server):
|
2021-09-25 13:56:23 -04:00
|
|
|
mock_mail_server.messages.clear()
|
2021-08-18 15:39:14 -04:00
|
|
|
status, data = client.post('/api/members', json={
|
2021-12-11 16:30:18 -05:00
|
|
|
'uid': 'test1',
|
2021-08-18 15:39:14 -04:00
|
|
|
'cn': 'Test One',
|
2021-10-23 23:21:09 -04:00
|
|
|
'given_name': 'Test',
|
|
|
|
'sn': 'One',
|
2021-08-18 15:39:14 -04:00
|
|
|
'program': 'Math',
|
|
|
|
'terms': ['s2021'],
|
2021-12-11 16:30:18 -05:00
|
|
|
'forwarding_addresses': ['test1@uwaterloo.internal'],
|
2021-08-18 15:39:14 -04:00
|
|
|
})
|
|
|
|
assert status == 200
|
|
|
|
assert data[-1]['status'] == 'completed'
|
|
|
|
yield status, data
|
2021-12-11 16:30:18 -05:00
|
|
|
status, data = client.delete('/api/members/test1')
|
2021-08-18 15:39:14 -04:00
|
|
|
assert status == 200
|
|
|
|
assert data[-1]['status'] == 'completed'
|
|
|
|
|
|
|
|
|
2021-12-11 16:30:18 -05:00
|
|
|
@pytest.fixture(scope='function')
|
2021-08-18 15:39:14 -04:00
|
|
|
def create_user_result(create_user_resp):
|
|
|
|
# convenience method
|
|
|
|
_, data = create_user_resp
|
|
|
|
return data[-1]['result']
|
|
|
|
|
|
|
|
|
2021-09-25 13:56:23 -04:00
|
|
|
def test_api_create_user(cfg, create_user_resp, mock_mail_server):
|
2021-08-18 15:39:14 -04:00
|
|
|
_, data = create_user_resp
|
|
|
|
min_uid = cfg.get('members_min_id')
|
|
|
|
expected = [
|
|
|
|
{"status": "in progress", "operation": "add_user_to_ldap"},
|
|
|
|
{"status": "in progress", "operation": "add_group_to_ldap"},
|
|
|
|
{"status": "in progress", "operation": "add_user_to_kerberos"},
|
|
|
|
{"status": "in progress", "operation": "create_home_dir"},
|
|
|
|
{"status": "in progress", "operation": "set_forwarding_addresses"},
|
|
|
|
{"status": "in progress", "operation": "send_welcome_message"},
|
|
|
|
{"status": "in progress", "operation": "subscribe_to_mailing_list"},
|
2021-08-22 01:44:41 -04:00
|
|
|
{"status": "in progress", "operation": "announce_new_user"},
|
2021-08-18 15:39:14 -04:00
|
|
|
{"status": "completed", "result": {
|
|
|
|
"cn": "Test One",
|
2021-10-23 23:21:09 -04:00
|
|
|
"given_name": "Test",
|
|
|
|
"sn": "One",
|
2021-12-11 16:30:18 -05:00
|
|
|
"uid": "test1",
|
2021-08-18 15:39:14 -04:00
|
|
|
"uid_number": min_uid,
|
|
|
|
"gid_number": min_uid,
|
|
|
|
"login_shell": "/bin/bash",
|
2021-12-11 16:30:18 -05:00
|
|
|
"home_directory": "/tmp/test_users/test1",
|
2021-08-18 15:39:14 -04:00
|
|
|
"is_club": False,
|
2021-10-23 10:23:43 -04:00
|
|
|
"is_club_rep": False,
|
2021-08-18 15:39:14 -04:00
|
|
|
"program": "Math",
|
|
|
|
"terms": ["s2021"],
|
2021-12-11 16:30:18 -05:00
|
|
|
"mail_local_addresses": ["test1@csclub.internal"],
|
|
|
|
"forwarding_addresses": ['test1@uwaterloo.internal'],
|
|
|
|
"password": "krb5",
|
|
|
|
"shadowExpire": None,
|
2021-08-18 15:39:14 -04:00
|
|
|
}},
|
|
|
|
]
|
|
|
|
assert data == expected
|
2021-09-25 13:56:23 -04:00
|
|
|
# Two messages should have been sent: a welcome message to the new member,
|
|
|
|
# and an announcement to the ceo mailing list
|
|
|
|
assert len(mock_mail_server.messages) == 2
|
2021-12-11 16:30:18 -05:00
|
|
|
assert mock_mail_server.messages[0]['to'] == 'test1@csclub.internal'
|
2021-09-25 13:56:23 -04:00
|
|
|
assert mock_mail_server.messages[1]['to'] == 'ceo@csclub.internal,ctdalek@csclub.internal'
|
|
|
|
mock_mail_server.messages.clear()
|
2021-08-18 15:39:14 -04:00
|
|
|
|
|
|
|
|
|
|
|
def test_api_next_uid(cfg, client, create_user_result):
|
|
|
|
min_uid = cfg.get('members_min_id')
|
|
|
|
_, data = client.post('/api/members', json={
|
2022-03-12 15:19:14 -05:00
|
|
|
'uid': 'test2',
|
2021-08-18 15:39:14 -04:00
|
|
|
'cn': 'Test Two',
|
2021-10-23 23:21:09 -04:00
|
|
|
'given_name': 'Test',
|
|
|
|
'sn': 'Two',
|
2021-08-13 20:11:56 -04:00
|
|
|
'program': 'Math',
|
|
|
|
'terms': ['s2021'],
|
|
|
|
})
|
2021-08-18 15:39:14 -04:00
|
|
|
assert data[-1]['status'] == 'completed'
|
|
|
|
result = data[-1]['result']
|
|
|
|
try:
|
|
|
|
assert result['uid_number'] == min_uid + 1
|
|
|
|
assert result['gid_number'] == min_uid + 1
|
|
|
|
finally:
|
2022-03-12 15:19:14 -05:00
|
|
|
client.delete('/api/members/test2')
|
2021-08-18 15:39:14 -04:00
|
|
|
|
|
|
|
|
|
|
|
def test_api_get_user(cfg, client, create_user_result):
|
2021-08-19 16:33:44 -04:00
|
|
|
old_data = create_user_result.copy()
|
2021-08-18 15:39:14 -04:00
|
|
|
uid = old_data['uid']
|
|
|
|
del old_data['password']
|
|
|
|
|
|
|
|
status, data = client.get(f'/api/members/{uid}')
|
|
|
|
assert status == 200
|
|
|
|
assert data == old_data
|
|
|
|
|
|
|
|
|
|
|
|
def test_api_patch_user(client, create_user_result):
|
|
|
|
data = create_user_result
|
|
|
|
uid = data['uid']
|
|
|
|
prev_login_shell = data['login_shell']
|
|
|
|
prev_forwarding_addresses = data['forwarding_addresses']
|
|
|
|
|
|
|
|
# replace login shell
|
|
|
|
new_shell = '/bin/sh'
|
|
|
|
status, data = client.patch(
|
|
|
|
f'/api/members/{uid}', json={'login_shell': new_shell})
|
|
|
|
assert status == 200
|
|
|
|
expected = [
|
|
|
|
{"status": "in progress", "operation": "replace_login_shell"},
|
|
|
|
{"status": "completed", "result": "OK"},
|
|
|
|
]
|
|
|
|
assert data == expected
|
|
|
|
|
|
|
|
# replace forwarding addresses
|
|
|
|
new_forwarding_addresses = [
|
|
|
|
'test@example1.internal', 'test@example2.internal',
|
|
|
|
]
|
|
|
|
status, data = client.patch(
|
|
|
|
f'/api/members/{uid}', json={
|
|
|
|
'forwarding_addresses': new_forwarding_addresses
|
|
|
|
}
|
|
|
|
)
|
|
|
|
assert status == 200
|
|
|
|
expected = [
|
|
|
|
{"status": "in progress", "operation": "replace_forwarding_addresses"},
|
|
|
|
{"status": "completed", "result": "OK"},
|
|
|
|
]
|
|
|
|
assert data == expected
|
|
|
|
|
|
|
|
# retrieve the user and make sure that both fields were changed
|
|
|
|
status, data = client.get(f'/api/members/{uid}')
|
|
|
|
assert status == 200
|
|
|
|
assert data['login_shell'] == new_shell
|
|
|
|
assert data['forwarding_addresses'] == new_forwarding_addresses
|
|
|
|
|
|
|
|
# replace (restore) both
|
|
|
|
status, data = client.patch(
|
|
|
|
f'/api/members/{uid}', json={
|
|
|
|
'login_shell': prev_login_shell,
|
|
|
|
'forwarding_addresses': prev_forwarding_addresses
|
|
|
|
}
|
|
|
|
)
|
|
|
|
assert status == 200
|
|
|
|
expected = [
|
|
|
|
{"status": "in progress", "operation": "replace_login_shell"},
|
|
|
|
{"status": "in progress", "operation": "replace_forwarding_addresses"},
|
|
|
|
{"status": "completed", "result": "OK"},
|
|
|
|
]
|
|
|
|
assert data == expected
|
|
|
|
|
|
|
|
|
|
|
|
def test_api_renew_user(cfg, client, create_user_result, ldap_conn):
|
2021-08-19 16:33:44 -04:00
|
|
|
data = create_user_result.copy()
|
2021-08-18 15:39:14 -04:00
|
|
|
uid = data['uid']
|
|
|
|
old_terms = data['terms']
|
|
|
|
old_non_member_terms = data.get('non_member_terms', [])
|
|
|
|
|
|
|
|
new_terms = ['f2021']
|
|
|
|
status, data = client.post(
|
|
|
|
f'/api/members/{uid}/renew', json={'terms': new_terms})
|
|
|
|
assert status == 200
|
2021-08-19 01:11:22 -04:00
|
|
|
assert data == {'terms_added': new_terms}
|
2021-08-18 15:39:14 -04:00
|
|
|
|
|
|
|
new_non_member_terms = ['w2022', 's2022']
|
|
|
|
status, data = client.post(
|
|
|
|
f'/api/members/{uid}/renew', json={'non_member_terms': new_non_member_terms})
|
|
|
|
assert status == 200
|
2021-08-19 01:11:22 -04:00
|
|
|
assert data == {'non_member_terms_added': new_non_member_terms}
|
2021-08-18 15:39:14 -04:00
|
|
|
|
|
|
|
# check that the changes were applied
|
|
|
|
_, data = client.get(f'/api/members/{uid}')
|
|
|
|
assert data['terms'] == old_terms + new_terms
|
|
|
|
assert data['non_member_terms'] == old_non_member_terms + new_non_member_terms
|
2021-10-23 10:23:43 -04:00
|
|
|
assert data['is_club_rep']
|
2021-08-18 15:39:14 -04:00
|
|
|
|
|
|
|
# cleanup
|
|
|
|
base_dn = cfg.get('ldap_users_base')
|
|
|
|
dn = f'uid={uid},{base_dn}'
|
|
|
|
changes = {
|
|
|
|
'term': [(ldap3.MODIFY_REPLACE, old_terms)],
|
|
|
|
'nonMemberTerm': [(ldap3.MODIFY_REPLACE, old_non_member_terms)],
|
2021-10-23 10:23:43 -04:00
|
|
|
'isClubRep': [(ldap3.MODIFY_REPLACE, [])],
|
2021-08-18 15:39:14 -04:00
|
|
|
}
|
|
|
|
ldap_conn.modify(dn, changes)
|
2021-08-13 20:11:56 -04:00
|
|
|
|
|
|
|
|
2021-08-18 15:39:14 -04:00
|
|
|
def test_api_reset_password(client, create_user_result):
|
|
|
|
uid = create_user_result['uid']
|
2021-12-11 16:30:18 -05:00
|
|
|
with patch.object(ceod.utils, 'gen_password') as gen_password_mock:
|
2021-08-18 15:39:14 -04:00
|
|
|
gen_password_mock.return_value = 'new_password'
|
|
|
|
status, data = client.post(f'/api/members/{uid}/pwreset')
|
|
|
|
assert status == 200
|
|
|
|
assert data['password'] == 'new_password'
|
|
|
|
# cleanup
|
|
|
|
status, data = client.post(f'/api/members/{uid}/pwreset')
|
|
|
|
assert status == 200
|
|
|
|
assert data['password'] == 'krb5'
|
2021-08-19 16:33:44 -04:00
|
|
|
|
|
|
|
|
|
|
|
def test_authz_check(client, create_user_result):
|
|
|
|
# non-staff members may not create users
|
|
|
|
status, data = client.post('/api/members', json={
|
2021-12-11 16:30:18 -05:00
|
|
|
'uid': 'test1', 'cn': 'Test One', 'given_name': 'Test',
|
2021-10-23 23:21:09 -04:00
|
|
|
'sn': 'One', 'terms': ['s2021'],
|
2021-08-19 16:33:44 -04:00
|
|
|
}, principal='regular1')
|
|
|
|
assert status == 403
|
|
|
|
|
|
|
|
# non-syscom members may not see forwarding addresses
|
|
|
|
old_data = create_user_result.copy()
|
|
|
|
uid = old_data['uid']
|
|
|
|
del old_data['password']
|
|
|
|
del old_data['forwarding_addresses']
|
|
|
|
_, data = client.get(f'/api/members/{uid}', principal='regular1')
|
|
|
|
assert data == old_data
|
2021-08-19 19:53:13 -04:00
|
|
|
|
|
|
|
# If we're syscom but we don't pass credentials, the request should fail
|
|
|
|
_, data = client.post('/api/members', json={
|
2021-12-11 16:30:18 -05:00
|
|
|
'uid': 'test1', 'cn': 'Test One', 'given_name': 'Test',
|
2021-10-23 23:21:09 -04:00
|
|
|
'sn': 'One', 'terms': ['s2021'],
|
2021-08-25 22:19:18 -04:00
|
|
|
}, principal='ctdalek', delegate=False)
|
2021-08-19 19:53:13 -04:00
|
|
|
assert data[-1]['status'] == 'aborted'
|
2021-12-11 16:30:18 -05:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('term_attr', ['terms', 'non_member_terms'])
|
2022-01-01 12:15:32 -05:00
|
|
|
def test_expire(client, new_user, term_attr, syscom_group, ldap_conn):
|
|
|
|
assert new_user.shadowExpire is None
|
|
|
|
current_term = Term.current()
|
|
|
|
start_of_current_term = current_term.to_datetime()
|
|
|
|
|
|
|
|
def reset_terms():
|
|
|
|
if term_attr == 'terms':
|
|
|
|
attr = 'term'
|
|
|
|
else:
|
|
|
|
attr = 'nonMemberTerm'
|
|
|
|
changes = {
|
|
|
|
attr: [(ldap3.MODIFY_REPLACE, [str(current_term)])]
|
|
|
|
}
|
|
|
|
dn = new_user.ldap_srv.uid_to_dn(new_user.uid)
|
|
|
|
ldap_conn.modify(dn, changes)
|
|
|
|
if term_attr == 'terms':
|
|
|
|
new_user.terms = [str(current_term)]
|
|
|
|
else:
|
|
|
|
new_user.non_member_terms = [str(current_term)]
|
|
|
|
|
2021-12-11 16:30:18 -05:00
|
|
|
# test_date, should_expire
|
|
|
|
test_cases = [
|
|
|
|
# same term, membership is still valid
|
|
|
|
(start_of_current_term + datetime.timedelta(days=90), False),
|
|
|
|
# first month of next term, grace period is activated
|
|
|
|
(start_of_current_term + datetime.timedelta(days=130), False),
|
|
|
|
# second month of next term, membership is now invalid
|
|
|
|
(start_of_current_term + datetime.timedelta(days=160), True),
|
|
|
|
# next next term, membership is definitely invalid
|
|
|
|
(start_of_current_term + datetime.timedelta(days=250), True),
|
|
|
|
]
|
2022-01-01 12:15:32 -05:00
|
|
|
uid = new_user.uid
|
2021-12-11 16:30:18 -05:00
|
|
|
|
|
|
|
for test_date, should_expire in test_cases:
|
2022-01-01 12:15:32 -05:00
|
|
|
with patch.object(ceo_common.utils, 'get_current_datetime') as datetime_mock:
|
|
|
|
user = new_user.to_dict()
|
2021-12-11 16:30:18 -05:00
|
|
|
datetime_mock.return_value = test_date
|
|
|
|
|
|
|
|
status, data = client.post('/api/members/expire?dry_run=yes')
|
|
|
|
assert status == 200
|
|
|
|
assert (data == [uid]) == should_expire
|
|
|
|
|
|
|
|
_, user = client.get(f'/api/members/{uid}')
|
|
|
|
assert user['shadowExpire'] is None
|
|
|
|
|
|
|
|
status, data = client.post('/api/members/expire')
|
|
|
|
assert status == 200
|
|
|
|
assert (data == [uid]) == should_expire
|
|
|
|
|
|
|
|
_, user = client.get(f'/api/members/{uid}')
|
|
|
|
assert (user['shadowExpire'] is not None) == should_expire
|
|
|
|
|
|
|
|
if not should_expire:
|
|
|
|
continue
|
|
|
|
|
|
|
|
term = Term.from_datetime(test_date)
|
|
|
|
status, _ = client.post(f'/api/members/{uid}/renew', json={term_attr: [str(term)]})
|
|
|
|
assert status == 200
|
|
|
|
|
|
|
|
_, user = client.get(f'/api/members/{uid}')
|
|
|
|
assert user['shadowExpire'] is None
|
2022-01-01 12:15:32 -05:00
|
|
|
reset_terms()
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize('in_syscom', [True, False])
|
|
|
|
def test_expire_syscom_member(client, new_user, syscom_group, g_admin_ctx, ldap_conn, in_syscom):
|
|
|
|
uid = new_user.uid
|
|
|
|
start_of_current_term = Term.current().to_datetime()
|
|
|
|
if in_syscom:
|
|
|
|
group_dn = new_user.ldap_srv.group_cn_to_dn('syscom')
|
|
|
|
user_dn = new_user.ldap_srv.uid_to_dn(uid)
|
|
|
|
changes = {
|
|
|
|
'uniqueMember': [(ldap3.MODIFY_ADD, [user_dn])]
|
|
|
|
}
|
|
|
|
ldap_conn.modify(group_dn, changes)
|
|
|
|
with patch.object(ceo_common.utils, 'get_current_datetime') as datetime_mock:
|
|
|
|
datetime_mock.return_value = start_of_current_term + datetime.timedelta(days=160)
|
|
|
|
status, data = client.post('/api/members/expire')
|
|
|
|
assert status == 200
|
|
|
|
if in_syscom:
|
|
|
|
assert data == []
|
|
|
|
else:
|
|
|
|
assert data == [uid]
|
2022-03-12 15:19:14 -05:00
|
|
|
|
|
|
|
|
|
|
|
def test_office_member(cfg, client):
|
|
|
|
admin_principal = cfg.get('ldap_admin_principal')
|
|
|
|
ccache_file = cfg.get('ldap_admin_principal_ccache')
|
|
|
|
if os.path.isfile(ccache_file):
|
|
|
|
os.unlink(ccache_file)
|
|
|
|
body = {
|
|
|
|
'uid': 'test3',
|
|
|
|
'cn': 'Test Three',
|
|
|
|
'given_name': 'Test',
|
|
|
|
'sn': 'Three',
|
|
|
|
'program': 'Math',
|
|
|
|
'terms': ['w2022'],
|
|
|
|
'forwarding_addresses': ['test3@uwaterloo.internal'],
|
|
|
|
}
|
|
|
|
status, data = client.post('/api/members', json=body, principal='office1')
|
|
|
|
assert status == 200 and data[-1]['status'] == 'completed'
|
|
|
|
|
|
|
|
# Make sure new admin creds were obtained
|
|
|
|
assert os.path.isfile(ccache_file)
|
|
|
|
os.unlink(ccache_file)
|
|
|
|
|
|
|
|
# Obtain new creds which expire
|
|
|
|
subprocess.run([
|
|
|
|
'kinit', '-k', '-p', admin_principal, '-l', '1'
|
|
|
|
], check=True, env={'KRB5CCNAME': 'FILE:' + ccache_file})
|
|
|
|
old_mtime = os.stat(ccache_file).st_mtime
|
|
|
|
# Wait for the ticket to expire
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
status, _ = client.post(
|
|
|
|
'/api/members/test3/renew',
|
|
|
|
json={'terms': ['s2022']}, principal='office1')
|
|
|
|
assert status == 200
|
|
|
|
|
|
|
|
# Make sure new admin creds were obtained
|
|
|
|
assert os.stat(ccache_file).st_mtime > old_mtime
|
|
|
|
|
|
|
|
status, _ = client.delete('/api/members/test3')
|
|
|
|
assert status == 200
|
2022-06-30 20:02:06 -04:00
|
|
|
|
|
|
|
|
|
|
|
def test_membership_renewal_reminder(client, mock_mail_server):
|
|
|
|
uids = ['test3', 'test4']
|
|
|
|
# fast-forward by one term so that we don't clash with the other users
|
|
|
|
# created by other tests
|
|
|
|
term = Term.current() + 1
|
|
|
|
with patch.object(ceo_common.utils, 'get_current_datetime') as datetime_mock:
|
|
|
|
datetime_mock.return_value = term.to_datetime()
|
|
|
|
for uid in uids:
|
|
|
|
body = {
|
|
|
|
'uid': uid,
|
|
|
|
'cn': 'John Doe',
|
|
|
|
'given_name': 'John',
|
|
|
|
'sn': 'Doe',
|
|
|
|
'program': 'Math',
|
|
|
|
'terms': [str(term)],
|
|
|
|
'forwarding_addresses': [uid + '@uwaterloo.internal'],
|
|
|
|
}
|
|
|
|
status, data = client.post('/api/members', json=body)
|
|
|
|
assert status == 200 and data[-1]['status'] == 'completed'
|
|
|
|
mock_mail_server.messages.clear()
|
|
|
|
|
|
|
|
# Members were freshly created - nobody should be expirable
|
|
|
|
status, data = client.post('/api/members/remindexpire')
|
|
|
|
assert status == 200
|
|
|
|
assert data == []
|
|
|
|
|
|
|
|
next_term = term + 1
|
|
|
|
datetime_mock.return_value = next_term.to_datetime() + datetime.timedelta(days=7)
|
|
|
|
status, data = client.post('/api/members/remindexpire?dry_run=true')
|
|
|
|
assert status == 200
|
|
|
|
assert sorted(uids) == sorted(data)
|
|
|
|
# dry run - no messages should have been sent
|
|
|
|
assert len(mock_mail_server.messages) == 0
|
|
|
|
|
|
|
|
status, data = client.post('/api/members/remindexpire')
|
|
|
|
assert status == 200
|
|
|
|
assert len(mock_mail_server.messages) == len(uids)
|
|
|
|
assert (
|
|
|
|
[uid + '@csclub.internal' for uid in sorted(uids)]
|
|
|
|
== sorted([msg['to'] for msg in mock_mail_server.messages])
|
|
|
|
)
|
|
|
|
|
|
|
|
# Renew only one of the expiring users
|
|
|
|
status, _ = client.post(f'/api/members/{uids[0]}/renew', json={'terms': [str(next_term)]})
|
|
|
|
assert status == 200
|
|
|
|
status, data = client.post('/api/members/remindexpire?dry_run=true')
|
|
|
|
assert status == 200
|
|
|
|
assert len(data) == len(uids) - 1
|
|
|
|
|
|
|
|
datetime_mock.return_value = next_term.to_datetime() + datetime.timedelta(days=40)
|
|
|
|
status, data = client.post('/api/members/remindexpire')
|
|
|
|
assert status == 200
|
|
|
|
# one-month grace period has passed - no messages should be sent out
|
|
|
|
assert data == []
|
|
|
|
|
|
|
|
next_next_term = next_term + 1
|
|
|
|
datetime_mock.return_value = next_next_term.to_datetime()
|
|
|
|
status, data = client.post('/api/members/remindexpire?dry_run=true')
|
|
|
|
assert status == 200
|
|
|
|
# only the user who renewed last term should get a reminder
|
|
|
|
assert data == [uids[0]]
|
|
|
|
|
|
|
|
for uid in uids:
|
|
|
|
status, _ = client.delete(f'/api/members/{uid}')
|
|
|
|
assert status == 200
|
|
|
|
mock_mail_server.messages.clear()
|