4.21.3
This commit is contained in:
0
app/tests/__init__.py
Normal file
0
app/tests/__init__.py
Normal file
0
app/tests/api/__init__.py
Normal file
0
app/tests/api/__init__.py
Normal file
681
app/tests/api/test_alias.py
Normal file
681
app/tests/api/test_alias.py
Normal file
@ -0,0 +1,681 @@
|
||||
import arrow
|
||||
from flask import url_for
|
||||
|
||||
# Need to import directly from config to allow modification from the tests
|
||||
from app import config
|
||||
from app.db import Session
|
||||
from app.email_utils import is_reverse_alias
|
||||
from app.models import User, Alias, Contact, EmailLog, Mailbox
|
||||
from tests.api.utils import get_new_user_and_api_key
|
||||
from tests.utils import login, random_domain
|
||||
|
||||
|
||||
def test_get_aliases_error_without_pagination(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.get_aliases"), headers={"Authentication": api_key.code}
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json["error"]
|
||||
|
||||
|
||||
def test_get_aliases_with_pagination(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
# create more aliases than config.PAGE_LIMIT
|
||||
for _ in range(config.PAGE_LIMIT + 1):
|
||||
Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
# get aliases on the 1st page, should return config.PAGE_LIMIT aliases
|
||||
r = flask_client.get(
|
||||
url_for("api.get_aliases", page_id=0), headers={"Authentication": api_key.code}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert len(r.json["aliases"]) == config.PAGE_LIMIT
|
||||
|
||||
# assert returned field
|
||||
for a in r.json["aliases"]:
|
||||
assert "id" in a
|
||||
assert "email" in a
|
||||
assert "creation_date" in a
|
||||
assert "creation_timestamp" in a
|
||||
assert "nb_forward" in a
|
||||
assert "nb_block" in a
|
||||
assert "nb_reply" in a
|
||||
assert "enabled" in a
|
||||
assert "note" in a
|
||||
|
||||
# get aliases on the 2nd page, should return 2 aliases
|
||||
# as the total number of aliases is config.PAGE_LIMIT +2
|
||||
# 1 alias is created when user is created
|
||||
r = flask_client.get(
|
||||
url_for("api.get_aliases", page_id=1), headers={"Authentication": api_key.code}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert len(r.json["aliases"]) == 2
|
||||
|
||||
|
||||
def test_get_aliases_query(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
# create more aliases than config.PAGE_LIMIT
|
||||
Alias.create_new(user, "prefix1")
|
||||
Alias.create_new(user, "prefix2")
|
||||
Session.commit()
|
||||
|
||||
# get aliases without query, should return 3 aliases as one alias is created when user is created
|
||||
r = flask_client.get(
|
||||
url_for("api.get_aliases", page_id=0), headers={"Authentication": api_key.code}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert len(r.json["aliases"]) == 3
|
||||
|
||||
# get aliases with "prefix1" query, should return 1 alias
|
||||
r = flask_client.get(
|
||||
url_for("api.get_aliases", page_id=0),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"query": "prefix1"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert len(r.json["aliases"]) == 1
|
||||
|
||||
|
||||
def test_get_aliases_v2(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
a0 = Alias.create_new(user, "prefix0")
|
||||
a1 = Alias.create_new(user, "prefix1")
|
||||
Session.commit()
|
||||
|
||||
# << Aliases have no activity >>
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0")
|
||||
assert r.status_code == 200
|
||||
|
||||
r0 = r.json["aliases"][0]
|
||||
assert "name" in r0
|
||||
|
||||
# make sure a1 is returned before a0
|
||||
assert r0["email"].startswith("prefix1")
|
||||
assert "id" in r0["mailbox"]
|
||||
assert "email" in r0["mailbox"]
|
||||
|
||||
assert r0["mailboxes"]
|
||||
for mailbox in r0["mailboxes"]:
|
||||
assert "id" in mailbox
|
||||
assert "email" in mailbox
|
||||
|
||||
assert "support_pgp" in r0
|
||||
assert not r0["support_pgp"]
|
||||
|
||||
assert "disable_pgp" in r0
|
||||
assert not r0["disable_pgp"]
|
||||
|
||||
# << Alias has some activities >>
|
||||
c0 = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=a0.id,
|
||||
website_email="c0@example.com",
|
||||
reply_email="re0@SL",
|
||||
commit=True,
|
||||
)
|
||||
EmailLog.create(
|
||||
contact_id=c0.id, user_id=user.id, alias_id=c0.alias_id, commit=True
|
||||
)
|
||||
|
||||
# a1 has more recent activity
|
||||
c1 = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=a1.id,
|
||||
website_email="c1@example.com",
|
||||
reply_email="re1@SL",
|
||||
commit=True,
|
||||
)
|
||||
EmailLog.create(
|
||||
contact_id=c1.id, user_id=user.id, alias_id=c1.alias_id, commit=True
|
||||
)
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0")
|
||||
assert r.status_code == 200
|
||||
|
||||
r0 = r.json["aliases"][0]
|
||||
|
||||
assert r0["latest_activity"]["action"] == "forward"
|
||||
assert "timestamp" in r0["latest_activity"]
|
||||
|
||||
assert r0["latest_activity"]["contact"]["email"] == "c1@example.com"
|
||||
assert "name" in r0["latest_activity"]["contact"]
|
||||
assert "reverse_alias" in r0["latest_activity"]["contact"]
|
||||
assert "pinned" in r0
|
||||
|
||||
|
||||
def test_get_pinned_aliases_v2(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
a0 = Alias.create_new(user, "prefix0")
|
||||
a0.pinned = True
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0")
|
||||
assert r.status_code == 200
|
||||
# the default alias (created when user is created) and a0 are returned
|
||||
assert len(r.json["aliases"]) == 2
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0&pinned=true")
|
||||
assert r.status_code == 200
|
||||
# only a0 is returned
|
||||
assert len(r.json["aliases"]) == 1
|
||||
assert r.json["aliases"][0]["id"] == a0.id
|
||||
|
||||
|
||||
def test_get_disabled_aliases_v2(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
a0 = Alias.create_new(user, "prefix0")
|
||||
a0.enabled = False
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0")
|
||||
assert r.status_code == 200
|
||||
# the default alias (created when user is created) and a0 are returned
|
||||
assert len(r.json["aliases"]) == 2
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0&disabled=true")
|
||||
assert r.status_code == 200
|
||||
# only a0 is returned
|
||||
assert len(r.json["aliases"]) == 1
|
||||
assert r.json["aliases"][0]["id"] == a0.id
|
||||
|
||||
|
||||
def test_get_enabled_aliases_v2(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
a0 = Alias.create_new(user, "prefix0")
|
||||
a0.enabled = False
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0")
|
||||
assert r.status_code == 200
|
||||
# the default alias (created when user is created) and a0 are returned
|
||||
assert len(r.json["aliases"]) == 2
|
||||
|
||||
r = flask_client.get("/api/v2/aliases?page_id=0&enabled=true")
|
||||
assert r.status_code == 200
|
||||
# only the first alias is returned
|
||||
assert len(r.json["aliases"]) == 1
|
||||
assert r.json["aliases"][0]["id"] != a0.id
|
||||
|
||||
|
||||
def test_delete_alias(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.delete(
|
||||
url_for("api.delete_alias", alias_id=alias.id),
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json == {"deleted": True}
|
||||
|
||||
|
||||
def test_toggle_alias(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.toggle_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json == {"enabled": False}
|
||||
|
||||
|
||||
def test_alias_activities(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
# create some alias log
|
||||
contact = Contact.create(
|
||||
website_email="marketing@example.com",
|
||||
reply_email="reply@a.b",
|
||||
alias_id=alias.id,
|
||||
user_id=alias.user_id,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
for _ in range(int(config.PAGE_LIMIT / 2)):
|
||||
EmailLog.create(
|
||||
contact_id=contact.id,
|
||||
is_reply=True,
|
||||
user_id=contact.user_id,
|
||||
alias_id=contact.alias_id,
|
||||
)
|
||||
|
||||
for _ in range(int(config.PAGE_LIMIT / 2) + 2):
|
||||
EmailLog.create(
|
||||
contact_id=contact.id,
|
||||
blocked=True,
|
||||
user_id=contact.user_id,
|
||||
alias_id=contact.alias_id,
|
||||
)
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.get_alias_activities", alias_id=alias.id, page_id=0),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert len(r.json["activities"]) == config.PAGE_LIMIT
|
||||
for ac in r.json["activities"]:
|
||||
assert ac["from"]
|
||||
assert ac["to"]
|
||||
assert ac["timestamp"]
|
||||
assert ac["action"]
|
||||
assert ac["reverse_alias"]
|
||||
assert ac["reverse_alias_address"]
|
||||
|
||||
# second page, should return 1 or 2 results only
|
||||
r = flask_client.get(
|
||||
url_for("api.get_alias_activities", alias_id=alias.id, page_id=1),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert len(r.json["activities"]) < 3
|
||||
|
||||
|
||||
def test_update_alias(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.put(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"note": "test note"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_update_alias_mailbox(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
mb = Mailbox.create(user_id=user.id, email="ab@cd.com", verified=True)
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.put(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"mailbox_id": mb.id},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
# fail when update with non-existing mailbox
|
||||
r = flask_client.put(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"mailbox_id": -1},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_update_alias_name(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.put(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"name": "Test Name"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
alias = Alias.get(alias.id)
|
||||
assert alias.name == "Test Name"
|
||||
|
||||
# update name with linebreak
|
||||
r = flask_client.put(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"name": "Test \nName"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
alias = Alias.get(alias.id)
|
||||
assert alias.name == "Test Name"
|
||||
|
||||
|
||||
def test_update_alias_mailboxes(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
mb1 = Mailbox.create(user_id=user.id, email="ab1@cd.com", verified=True)
|
||||
mb2 = Mailbox.create(user_id=user.id, email="ab2@cd.com", verified=True)
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.put(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"mailbox_ids": [mb1.id, mb2.id]},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
alias = Alias.get(alias.id)
|
||||
|
||||
assert alias.mailbox
|
||||
assert len(alias._mailboxes) == 1
|
||||
|
||||
# fail when update with empty mailboxes
|
||||
r = flask_client.put(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"mailbox_ids": []},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_update_disable_pgp(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
assert not alias.disable_pgp
|
||||
|
||||
r = flask_client.put(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"disable_pgp": True},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
alias = Alias.get(alias.id)
|
||||
assert alias.disable_pgp
|
||||
|
||||
|
||||
def test_update_pinned(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
assert not alias.pinned
|
||||
|
||||
r = flask_client.patch(
|
||||
url_for("api.update_alias", alias_id=alias.id),
|
||||
json={"pinned": True},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert alias.pinned
|
||||
|
||||
|
||||
def test_alias_contacts(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
# create some alias log
|
||||
for i in range(config.PAGE_LIMIT + 1):
|
||||
contact = Contact.create(
|
||||
website_email=f"marketing-{i}@example.com",
|
||||
reply_email=f"reply-{i}@a.b",
|
||||
alias_id=alias.id,
|
||||
user_id=alias.user_id,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
EmailLog.create(
|
||||
contact_id=contact.id,
|
||||
is_reply=True,
|
||||
user_id=contact.user_id,
|
||||
alias_id=contact.alias_id,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(f"/api/aliases/{alias.id}/contacts?page_id=0")
|
||||
|
||||
assert r.status_code == 200
|
||||
assert len(r.json["contacts"]) == config.PAGE_LIMIT
|
||||
for ac in r.json["contacts"]:
|
||||
assert ac["creation_date"]
|
||||
assert ac["creation_timestamp"]
|
||||
assert ac["last_email_sent_date"]
|
||||
assert ac["last_email_sent_timestamp"]
|
||||
assert ac["contact"]
|
||||
assert ac["reverse_alias"]
|
||||
assert ac["reverse_alias_address"]
|
||||
assert "block_forward" in ac
|
||||
|
||||
# second page, should return 1 result only
|
||||
r = flask_client.get(f"/api/aliases/{alias.id}/contacts?page_id=1")
|
||||
assert len(r.json["contacts"]) == 1
|
||||
|
||||
|
||||
def test_create_contact_route(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"contact": "First Last <first@example.com>"},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["contact"] == "first@example.com"
|
||||
assert "creation_date" in r.json
|
||||
assert "creation_timestamp" in r.json
|
||||
assert r.json["last_email_sent_date"] is None
|
||||
assert r.json["last_email_sent_timestamp"] is None
|
||||
assert r.json["reverse_alias"]
|
||||
assert r.json["reverse_alias_address"]
|
||||
assert r.json["existed"] is False
|
||||
|
||||
# re-add a contact, should return 200
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"contact": "First2 Last2 <first@example.com>"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json["existed"]
|
||||
|
||||
|
||||
def test_create_contact_route_invalid_alias(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
other_user, other_api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(other_user)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"contact": "First Last <first@example.com>"},
|
||||
)
|
||||
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_create_contact_route_free_users(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
# On trial, should be ok
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"contact": f"First Last <first@{random_domain()}>"},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
|
||||
# End trial but allow via flags for older free users
|
||||
user.trial_end = arrow.now()
|
||||
user.flags = 0
|
||||
Session.commit()
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"contact": f"First Last <first@{random_domain()}>"},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
|
||||
# End trial and disallow for new free users. Config should allow it
|
||||
user.flags = User.FLAG_FREE_DISABLE_CREATE_ALIAS
|
||||
Session.commit()
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"contact": f"First Last <first@{random_domain()}>"},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
|
||||
# Set the global config to disable free users from create contacts
|
||||
config.DISABLE_CREATE_CONTACTS_FOR_FREE_USERS = True
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"contact": f"First Last <first@{random_domain()}>"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
config.DISABLE_CREATE_CONTACTS_FOR_FREE_USERS = False
|
||||
|
||||
|
||||
def test_create_contact_route_empty_contact_address(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
json={"contact": ""},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json["error"] == "Empty address is not a valid email address"
|
||||
|
||||
|
||||
def test_create_contact_route_invalid_contact_email(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.create_contact_route", alias_id=alias.id),
|
||||
json={"contact": "@gmail.com"},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json["error"] == "@gmail.com is not a valid email address"
|
||||
|
||||
|
||||
def test_delete_contact(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
contact = Contact.create(
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="reply+random@sl.io",
|
||||
user_id=alias.user_id,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.delete(
|
||||
url_for("api.delete_contact", contact_id=contact.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json == {"deleted": True}
|
||||
|
||||
|
||||
def test_get_alias(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
# create more aliases than config.PAGE_LIMIT
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
# get aliases on the 1st page, should return config.PAGE_LIMIT aliases
|
||||
r = flask_client.get(
|
||||
url_for("api.get_alias", alias_id=alias.id),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
# assert returned field
|
||||
res = r.json
|
||||
assert "id" in res
|
||||
assert "email" in res
|
||||
assert "creation_date" in res
|
||||
assert "creation_timestamp" in res
|
||||
assert "nb_forward" in res
|
||||
assert "nb_block" in res
|
||||
assert "nb_reply" in res
|
||||
assert "enabled" in res
|
||||
assert "note" in res
|
||||
assert "pinned" in res
|
||||
|
||||
|
||||
def test_is_reverse_alias(flask_client):
|
||||
assert is_reverse_alias("ra+abcd@sl.local")
|
||||
assert is_reverse_alias("reply+abcd@sl.local")
|
||||
|
||||
assert not is_reverse_alias("ra+abcd@test.org")
|
||||
assert not is_reverse_alias("reply+abcd@test.org")
|
||||
assert not is_reverse_alias("abcd@test.org")
|
||||
|
||||
|
||||
def test_toggle_contact(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
contact = Contact.create(
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="reply+random@sl.io",
|
||||
user_id=alias.user_id,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(f"/api/contacts/{contact.id}/toggle")
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json == {"block_forward": True}
|
||||
|
||||
|
||||
def test_get_aliases_disabled_account(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
r = flask_client.get(
|
||||
"/api/v2/aliases?page_id=0",
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
user.disabled = True
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
"/api/v2/aliases?page_id=0",
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.status_code == 403
|
127
app/tests/api/test_alias_options.py
Normal file
127
app/tests/api/test_alias_options.py
Normal file
@ -0,0 +1,127 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.models import AliasUsedOn, Alias
|
||||
from tests.api.utils import get_new_user_and_api_key
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_different_scenarios_v4(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
# <<< without hostname >>>
|
||||
r = flask_client.get(
|
||||
"/api/v4/alias/options", headers={"Authentication": api_key.code}
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
assert r.json["can_create"]
|
||||
assert r.json["suffixes"]
|
||||
assert r.json["prefix_suggestion"] == "" # no hostname => no suggestion
|
||||
|
||||
# <<< with hostname >>>
|
||||
r = flask_client.get(
|
||||
url_for("api.options_v4", hostname="www.test.com"),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.json["prefix_suggestion"] == "test"
|
||||
|
||||
# <<< with recommendation >>>
|
||||
alias = Alias.create_new(user, prefix="test")
|
||||
Session.commit()
|
||||
AliasUsedOn.create(
|
||||
alias_id=alias.id, hostname="www.test.com", user_id=alias.user_id
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.options_v4", hostname="www.test.com"),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.json["recommendation"]["alias"] == alias.email
|
||||
assert r.json["recommendation"]["hostname"] == "www.test.com"
|
||||
|
||||
|
||||
def test_different_scenarios_v4_2(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
# <<< without hostname >>>
|
||||
r = flask_client.get(
|
||||
url_for("api.options_v4"), headers={"Authentication": api_key.code}
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
assert r.json["can_create"]
|
||||
assert r.json["suffixes"]
|
||||
assert r.json["prefix_suggestion"] == "" # no hostname => no suggestion
|
||||
|
||||
for (suffix, signed_suffix) in r.json["suffixes"]:
|
||||
assert signed_suffix.startswith(suffix)
|
||||
|
||||
# <<< with hostname >>>
|
||||
r = flask_client.get(
|
||||
url_for("api.options_v4", hostname="www.test.com"),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.json["prefix_suggestion"] == "test"
|
||||
|
||||
# <<< with recommendation >>>
|
||||
alias = Alias.create_new(user, prefix="test")
|
||||
Session.commit()
|
||||
AliasUsedOn.create(
|
||||
alias_id=alias.id, hostname="www.test.com", user_id=alias.user_id
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.options_v4", hostname="www.test.com"),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.json["recommendation"]["alias"] == alias.email
|
||||
assert r.json["recommendation"]["hostname"] == "www.test.com"
|
||||
|
||||
|
||||
def test_different_scenarios_v5(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
# <<< without hostname >>>
|
||||
r = flask_client.get("/api/v5/alias/options")
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
assert r.json["can_create"]
|
||||
assert r.json["suffixes"]
|
||||
assert r.json["prefix_suggestion"] == "" # no hostname => no suggestion
|
||||
|
||||
for suffix_payload in r.json["suffixes"]:
|
||||
suffix, signed_suffix = (
|
||||
suffix_payload["suffix"],
|
||||
suffix_payload["signed_suffix"],
|
||||
)
|
||||
assert signed_suffix.startswith(suffix)
|
||||
assert "is_custom" in suffix_payload
|
||||
assert "is_premium" in suffix_payload
|
||||
|
||||
# <<< with hostname >>>
|
||||
r = flask_client.get("/api/v5/alias/options?hostname=www.test.com")
|
||||
assert r.json["prefix_suggestion"] == "test"
|
||||
|
||||
# <<< with hostname with 2 parts TLD, for example wwww.numberoneshoes.co.nz >>>
|
||||
r = flask_client.get("/api/v5/alias/options?hostname=wwww.numberoneshoes.co.nz")
|
||||
assert r.json["prefix_suggestion"] == "numberoneshoes"
|
||||
|
||||
# <<< with recommendation >>>
|
||||
alias = Alias.create_new(user, prefix="test")
|
||||
Session.commit()
|
||||
AliasUsedOn.create(
|
||||
alias_id=alias.id, hostname="www.test.com", user_id=alias.user_id
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(url_for("api.options_v4", hostname="www.test.com"))
|
||||
assert r.json["recommendation"]["alias"] == alias.email
|
||||
assert r.json["recommendation"]["hostname"] == "www.test.com"
|
201
app/tests/api/test_apple.py
Normal file
201
app/tests/api/test_apple.py
Normal file
File diff suppressed because one or more lines are too long
272
app/tests/api/test_auth.py
Normal file
272
app/tests/api/test_auth.py
Normal file
@ -0,0 +1,272 @@
|
||||
import pytest
|
||||
import unicodedata
|
||||
from flask import url_for
|
||||
|
||||
from app import config
|
||||
from app.db import Session
|
||||
from app.models import User, AccountActivation
|
||||
from tests.utils import random_email
|
||||
|
||||
PASSWORD_1 = "Aurélie"
|
||||
PASSWORD_2 = unicodedata.normalize("NFKD", PASSWORD_1)
|
||||
assert PASSWORD_1 != PASSWORD_2
|
||||
|
||||
|
||||
def setup_module():
|
||||
config.SKIP_MX_LOOKUP_ON_CHECK = True
|
||||
|
||||
|
||||
def teardown_module():
|
||||
config.SKIP_MX_LOOKUP_ON_CHECK = False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mfa", (True, False), ids=("MFA", "no MFA"))
|
||||
def test_auth_login_success(flask_client, mfa: bool):
|
||||
email = random_email()
|
||||
User.create(
|
||||
email=email,
|
||||
password=PASSWORD_1,
|
||||
name="Test User",
|
||||
activated=True,
|
||||
enable_otp=mfa,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"email": email,
|
||||
"password": PASSWORD_2,
|
||||
"device": "Test Device",
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["name"] == "Test User"
|
||||
assert r.json["email"]
|
||||
|
||||
if mfa:
|
||||
assert r.json["api_key"] is None
|
||||
assert r.json["mfa_enabled"]
|
||||
assert r.json["mfa_key"]
|
||||
else:
|
||||
assert r.json["api_key"]
|
||||
assert not r.json["mfa_enabled"]
|
||||
assert r.json["mfa_key"] is None
|
||||
|
||||
|
||||
def test_auth_login_device_exist(flask_client):
|
||||
email = random_email()
|
||||
User.create(email=email, password="password", name="Test User", activated=True)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_login"),
|
||||
json={
|
||||
"email": email,
|
||||
"password": "password",
|
||||
"device": "Test Device",
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
api_key = r.json["api_key"]
|
||||
assert not r.json["mfa_enabled"]
|
||||
assert r.json["mfa_key"] is None
|
||||
assert r.json["name"] == "Test User"
|
||||
|
||||
# same device, should return same api_key
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_login"),
|
||||
json={
|
||||
"email": email,
|
||||
"password": "password",
|
||||
"device": "Test Device",
|
||||
},
|
||||
)
|
||||
assert r.json["api_key"] == api_key
|
||||
|
||||
|
||||
def test_auth_register_success(flask_client):
|
||||
email = random_email()
|
||||
assert AccountActivation.first() is None
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_register"),
|
||||
json={"email": email, "password": "password"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["msg"]
|
||||
|
||||
# make sure an activation code is created
|
||||
act_code = AccountActivation.first()
|
||||
assert act_code
|
||||
assert len(act_code.code) == 6
|
||||
assert act_code.tries == 3
|
||||
|
||||
|
||||
def test_auth_register_too_short_password(flask_client):
|
||||
email = random_email()
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_register"),
|
||||
json={"email": email, "password": "short"},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json["error"] == "password too short"
|
||||
|
||||
|
||||
def test_auth_register_too_long_password(flask_client):
|
||||
email = random_email()
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_register"),
|
||||
json={"email": email, "password": "0123456789" * 11},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json["error"] == "password too long"
|
||||
|
||||
|
||||
def test_auth_activate_success(flask_client):
|
||||
email = random_email()
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_register"),
|
||||
json={"email": email, "password": "password"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["msg"]
|
||||
|
||||
# get the activation code
|
||||
act_code = AccountActivation.first()
|
||||
assert act_code
|
||||
assert len(act_code.code) == 6
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_activate"),
|
||||
json={"email": email, "code": act_code.code},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_auth_activate_wrong_email(flask_client):
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_activate"), json={"email": "abcd@gmail.com", "code": "123456"}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_auth_activate_user_already_activated(flask_client):
|
||||
email = random_email()
|
||||
User.create(email=email, password="password", name="Test User", activated=True)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_activate"), json={"email": email, "code": "123456"}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_auth_activate_wrong_code(flask_client):
|
||||
email = random_email()
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_register"),
|
||||
json={"email": email, "password": "password"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["msg"]
|
||||
|
||||
# get the activation code
|
||||
act_code = AccountActivation.first()
|
||||
assert act_code
|
||||
assert len(act_code.code) == 6
|
||||
assert act_code.tries == 3
|
||||
|
||||
# make sure to create a wrong code
|
||||
wrong_code = act_code.code + "123"
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_activate"),
|
||||
json={"email": email, "code": wrong_code},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
# make sure the nb tries decrements
|
||||
act_code = AccountActivation.first()
|
||||
assert act_code.tries == 2
|
||||
|
||||
|
||||
def test_auth_activate_too_many_wrong_code(flask_client):
|
||||
email = random_email()
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_register"),
|
||||
json={"email": email, "password": "password"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["msg"]
|
||||
|
||||
# get the activation code
|
||||
act_code = AccountActivation.first()
|
||||
assert act_code
|
||||
assert len(act_code.code) == 6
|
||||
assert act_code.tries == 3
|
||||
|
||||
# make sure to create a wrong code
|
||||
wrong_code = act_code.code + "123"
|
||||
|
||||
for _ in range(2):
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_activate"),
|
||||
json={"email": email, "code": wrong_code},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
# the activation code is deleted
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_activate"),
|
||||
json={"email": email, "code": wrong_code},
|
||||
)
|
||||
|
||||
assert r.status_code == 410
|
||||
|
||||
# make sure the nb tries decrements
|
||||
assert AccountActivation.first() is None
|
||||
|
||||
|
||||
def test_auth_reactivate_success(flask_client):
|
||||
email = random_email()
|
||||
User.create(email=email, password="password", name="Test User")
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(url_for("api.auth_reactivate"), json={"email": email})
|
||||
assert r.status_code == 200
|
||||
|
||||
# make sure an activation code is created
|
||||
act_code = AccountActivation.first()
|
||||
assert act_code
|
||||
assert len(act_code.code) == 6
|
||||
assert act_code.tries == 3
|
||||
|
||||
|
||||
def test_auth_login_forgot_password(flask_client):
|
||||
email = random_email()
|
||||
User.create(email=email, password="password", name="Test User", activated=True)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.forgot_password"),
|
||||
json={"email": email},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
# No such email, still return 200
|
||||
r = flask_client.post(
|
||||
url_for("api.forgot_password"),
|
||||
json={"email": random_email()},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
46
app/tests/api/test_auth_mfa.py
Normal file
46
app/tests/api/test_auth_mfa.py
Normal file
@ -0,0 +1,46 @@
|
||||
import pyotp
|
||||
from flask import url_for
|
||||
from itsdangerous import Signer
|
||||
|
||||
from app.config import FLASK_SECRET
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def test_auth_mfa_success(flask_client):
|
||||
user = create_new_user()
|
||||
user.enable_otp = True
|
||||
user.otp_secret = "base32secret3232"
|
||||
|
||||
totp = pyotp.TOTP(user.otp_secret)
|
||||
s = Signer(FLASK_SECRET)
|
||||
mfa_key = s.sign(str(user.id))
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_mfa"),
|
||||
json={"mfa_token": totp.now(), "mfa_key": mfa_key, "device": "Test Device"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["api_key"]
|
||||
assert r.json["email"]
|
||||
assert r.json["name"] == "Test User"
|
||||
|
||||
|
||||
def test_auth_wrong_mfa_key(flask_client):
|
||||
user = create_new_user()
|
||||
user.enable_otp = True
|
||||
user.otp_secret = "base32secret3232"
|
||||
|
||||
totp = pyotp.TOTP(user.otp_secret)
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.auth_mfa"),
|
||||
json={
|
||||
"mfa_token": totp.now(),
|
||||
"mfa_key": "wrong mfa key",
|
||||
"device": "Test Device",
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json["error"]
|
116
app/tests/api/test_custom_domain.py
Normal file
116
app/tests/api/test_custom_domain.py
Normal file
@ -0,0 +1,116 @@
|
||||
from app.alias_utils import delete_alias
|
||||
from app.models import CustomDomain, Alias, Mailbox
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_get_custom_domains(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
CustomDomain.create(user_id=user.id, domain="test1.org", verified=True, commit=True)
|
||||
CustomDomain.create(
|
||||
user_id=user.id, domain="test2.org", verified=False, commit=True
|
||||
)
|
||||
|
||||
r = flask_client.get(
|
||||
"/api/custom_domains",
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert len(r.json["custom_domains"]) == 2
|
||||
for domain in r.json["custom_domains"]:
|
||||
assert domain["domain_name"]
|
||||
assert domain["id"]
|
||||
assert domain["nb_alias"] == 0
|
||||
assert "is_verified" in domain
|
||||
assert "catch_all" in domain
|
||||
assert "name" in domain
|
||||
assert "random_prefix_generation" in domain
|
||||
assert domain["creation_date"]
|
||||
assert domain["creation_timestamp"]
|
||||
|
||||
assert domain["mailboxes"]
|
||||
for mailbox in domain["mailboxes"]:
|
||||
assert "id" in mailbox
|
||||
assert "email" in mailbox
|
||||
|
||||
|
||||
def test_update_custom_domains(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
d1 = CustomDomain.create(
|
||||
user_id=user.id, domain="test1.org", verified=True, commit=True
|
||||
)
|
||||
|
||||
# test update catch all
|
||||
assert d1.catch_all is False
|
||||
r = flask_client.patch(f"/api/custom_domains/{d1.id}", json={"catch_all": True})
|
||||
assert r.status_code == 200
|
||||
assert d1.catch_all is True
|
||||
|
||||
# make sure the full domain json is returned
|
||||
cd_json = r.json["custom_domain"]
|
||||
assert cd_json["domain_name"]
|
||||
assert cd_json["id"] == d1.id
|
||||
assert cd_json["nb_alias"] == 0
|
||||
assert "is_verified" in cd_json
|
||||
assert "catch_all" in cd_json
|
||||
assert "name" in cd_json
|
||||
assert "random_prefix_generation" in cd_json
|
||||
assert cd_json["creation_date"]
|
||||
assert cd_json["creation_timestamp"]
|
||||
|
||||
assert cd_json["mailboxes"]
|
||||
for mailbox in cd_json["mailboxes"]:
|
||||
assert "id" in mailbox
|
||||
assert "email" in mailbox
|
||||
|
||||
# test update random_prefix_generation
|
||||
assert d1.random_prefix_generation is False
|
||||
r = flask_client.patch(
|
||||
f"/api/custom_domains/{d1.id}", json={"random_prefix_generation": True}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert d1.random_prefix_generation is True
|
||||
|
||||
# test update name
|
||||
assert d1.name is None
|
||||
r = flask_client.patch(f"/api/custom_domains/{d1.id}", json={"name": "test name"})
|
||||
assert r.status_code == 200
|
||||
assert d1.name == "test name"
|
||||
|
||||
# test update mailboxes
|
||||
assert d1.mailboxes == [user.default_mailbox]
|
||||
mb = Mailbox.create(
|
||||
user_id=user.id, email="test@example.org", verified=True, commit=True
|
||||
)
|
||||
r = flask_client.patch(
|
||||
f"/api/custom_domains/{d1.id}", json={"mailbox_ids": [mb.id]}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert d1.mailboxes == [mb]
|
||||
|
||||
|
||||
def test_get_custom_domain_trash(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
cd = CustomDomain.create(
|
||||
user_id=user.id, domain="test1.org", verified=True, commit=True
|
||||
)
|
||||
|
||||
alias = Alias.create(
|
||||
user_id=user.id,
|
||||
email="first@test1.org",
|
||||
custom_domain_id=cd.id,
|
||||
mailbox_id=user.default_mailbox_id,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
delete_alias(alias, user)
|
||||
|
||||
r = flask_client.get(
|
||||
f"/api/custom_domains/{cd.id}/trash",
|
||||
)
|
||||
|
||||
for deleted_alias in r.json["aliases"]:
|
||||
assert deleted_alias["alias"]
|
||||
assert deleted_alias["deletion_timestamp"] > 0
|
141
app/tests/api/test_import_export.py
Normal file
141
app/tests/api/test_import_export.py
Normal file
@ -0,0 +1,141 @@
|
||||
from app.db import Session
|
||||
from app.import_utils import import_from_csv
|
||||
from app.models import (
|
||||
CustomDomain,
|
||||
Mailbox,
|
||||
Alias,
|
||||
BatchImport,
|
||||
File,
|
||||
)
|
||||
from tests.utils_test_alias import alias_export
|
||||
from tests.utils import login, random_domain, random_token
|
||||
|
||||
|
||||
def test_export(flask_client):
|
||||
alias_export(flask_client, "api.export_aliases")
|
||||
|
||||
|
||||
def test_import_no_mailboxes_no_domains(flask_client):
|
||||
# Create user
|
||||
user = login(flask_client)
|
||||
|
||||
# Check start state
|
||||
assert len(Alias.filter_by(user_id=user.id).all()) == 1 # Onboarding alias
|
||||
|
||||
alias_data = [
|
||||
"alias,note",
|
||||
"ebay@my-domain.com,Used on eBay",
|
||||
'facebook@my-domain.com,"Used on Facebook, Instagram."',
|
||||
]
|
||||
file = File.create(path=f"/{random_token()}", commit=True)
|
||||
batch_import = BatchImport.create(user_id=user.id, file_id=file.id, commit=True)
|
||||
|
||||
import_from_csv(batch_import, user, alias_data)
|
||||
|
||||
# Should have failed to import anything new because my-domain.com isn't registered
|
||||
assert len(Alias.filter_by(user_id=user.id).all()) == 1 # +0
|
||||
|
||||
|
||||
def test_import_no_mailboxes(flask_client):
|
||||
# Create user
|
||||
user = login(flask_client)
|
||||
|
||||
# Check start state
|
||||
assert len(Alias.filter_by(user_id=user.id).all()) == 1 # Onboarding alias
|
||||
|
||||
domain = random_domain()
|
||||
# Create domain
|
||||
CustomDomain.create(user_id=user.id, domain=domain, ownership_verified=True)
|
||||
Session.commit()
|
||||
|
||||
alias_data = [
|
||||
"alias,note",
|
||||
f"ebay@{domain},Used on eBay",
|
||||
f'facebook@{domain},"Used on Facebook, Instagram."',
|
||||
]
|
||||
|
||||
file = File.create(path=f"/{random_token()}", commit=True)
|
||||
batch_import = BatchImport.create(user_id=user.id, file_id=file.id)
|
||||
|
||||
import_from_csv(batch_import, user, alias_data)
|
||||
|
||||
assert len(Alias.filter_by(user_id=user.id).all()) == 3 # +2
|
||||
|
||||
|
||||
def test_import_no_domains(flask_client):
|
||||
# Create user
|
||||
user = login(flask_client)
|
||||
|
||||
# Check start state
|
||||
assert len(Alias.filter_by(user_id=user.id).all()) == 1 # Onboarding alias
|
||||
|
||||
alias_data = [
|
||||
"alias,note,mailboxes",
|
||||
"ebay@my-domain.com,Used on eBay,destination@my-destination-domain.com",
|
||||
'facebook@my-domain.com,"Used on Facebook, Instagram.",destination1@my-destination-domain.com destination2@my-destination-domain.com',
|
||||
]
|
||||
|
||||
file = File.create(path=f"/{random_token()}", commit=True)
|
||||
batch_import = BatchImport.create(user_id=user.id, file_id=file.id)
|
||||
|
||||
import_from_csv(batch_import, user, alias_data)
|
||||
|
||||
# Should have failed to import anything new because my-domain.com isn't registered
|
||||
assert len(Alias.filter_by(user_id=user.id).all()) == 1 # +0
|
||||
|
||||
|
||||
def test_import(flask_client):
|
||||
# Create user
|
||||
user = login(flask_client)
|
||||
|
||||
# Check start state
|
||||
assert len(Alias.filter_by(user_id=user.id).all()) == 1 # Onboarding alias
|
||||
|
||||
domain1 = random_domain()
|
||||
domain2 = random_domain()
|
||||
# Create domains
|
||||
CustomDomain.create(user_id=user.id, domain=domain1, ownership_verified=True)
|
||||
CustomDomain.create(user_id=user.id, domain=domain2, ownership_verified=True)
|
||||
Session.commit()
|
||||
|
||||
# Create mailboxes
|
||||
mailbox1 = Mailbox.create(
|
||||
user_id=user.id, email=f"destination@{domain2}", verified=True
|
||||
)
|
||||
mailbox2 = Mailbox.create(
|
||||
user_id=user.id, email=f"destination2@{domain2}", verified=True
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
alias_data = [
|
||||
"alias,note,mailboxes",
|
||||
f"ebay@{domain1},Used on eBay,destination@{domain2}",
|
||||
f'facebook@{domain1},"Used on Facebook, Instagram.",destination@{domain2} destination2@{domain2}',
|
||||
]
|
||||
|
||||
file = File.create(path=f"/{random_token()}", commit=True)
|
||||
batch_import = BatchImport.create(user_id=user.id, file_id=file.id)
|
||||
|
||||
import_from_csv(batch_import, user, alias_data)
|
||||
|
||||
aliases = Alias.filter_by(user_id=user.id).order_by(Alias.id).all()
|
||||
assert len(aliases) == 3 # +2
|
||||
|
||||
# aliases[0] is the onboarding alias, skip it
|
||||
|
||||
# eBay alias
|
||||
assert aliases[1].email == f"ebay@{domain1}"
|
||||
assert len(aliases[1].mailboxes) == 1
|
||||
# First one should be primary
|
||||
assert aliases[1].mailbox_id == mailbox1.id
|
||||
# Others are sorted
|
||||
assert aliases[1].mailboxes[0] == mailbox1
|
||||
|
||||
# Facebook alias
|
||||
assert aliases[2].email == f"facebook@{domain1}"
|
||||
assert len(aliases[2].mailboxes) == 2
|
||||
# First one should be primary
|
||||
assert aliases[2].mailbox_id == mailbox1.id
|
||||
# Others are sorted
|
||||
assert aliases[2].mailboxes[0] == mailbox2
|
||||
assert aliases[2].mailboxes[1] == mailbox1
|
191
app/tests/api/test_mailbox.py
Normal file
191
app/tests/api/test_mailbox.py
Normal file
@ -0,0 +1,191 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.models import Mailbox
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_create_mailbox(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/mailboxes",
|
||||
json={"email": "mailbox@gmail.com"},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
|
||||
assert r.json["email"] == "mailbox@gmail.com"
|
||||
assert r.json["verified"] is False
|
||||
assert r.json["id"] > 0
|
||||
assert r.json["default"] is False
|
||||
assert r.json["nb_alias"] == 0
|
||||
|
||||
# invalid email address
|
||||
r = flask_client.post(
|
||||
"/api/mailboxes",
|
||||
json={"email": "gmail.com"},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {"error": "gmail.com invalid"}
|
||||
|
||||
|
||||
def test_create_mailbox_fail_for_free_user(flask_client):
|
||||
user = login(flask_client)
|
||||
user.trial_end = None
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/mailboxes",
|
||||
json={"email": "mailbox@gmail.com"},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {"error": "Only premium plan can add additional mailbox"}
|
||||
|
||||
|
||||
def test_delete_mailbox(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
# create a mailbox
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.delete(
|
||||
f"/api/mailboxes/{mb.id}",
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_delete_default_mailbox(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
# assert user cannot delete the default mailbox
|
||||
r = flask_client.delete(
|
||||
url_for("api.delete_mailbox", mailbox_id=user.default_mailbox_id),
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_set_mailbox_as_default(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
mb = Mailbox.create(
|
||||
user_id=user.id, email="mb@gmail.com", verified=True, commit=True
|
||||
)
|
||||
assert user.default_mailbox_id != mb.id
|
||||
|
||||
r = flask_client.put(
|
||||
f"/api/mailboxes/{mb.id}",
|
||||
json={"default": True},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert user.default_mailbox_id == mb.id
|
||||
|
||||
# <<< Cannot set an unverified mailbox as default >>>
|
||||
mb.verified = False
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.put(
|
||||
f"/api/mailboxes/{mb.id}",
|
||||
json={"default": True},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {"error": "Unverified mailbox cannot be used as default mailbox"}
|
||||
|
||||
|
||||
def test_update_mailbox_email(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
# create a mailbox
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.put(
|
||||
f"/api/mailboxes/{mb.id}",
|
||||
json={"email": "new-email@gmail.com"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
mb = Mailbox.get(mb.id)
|
||||
assert mb.new_email == "new-email@gmail.com"
|
||||
|
||||
|
||||
def test_cancel_mailbox_email_change(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
# create a mailbox
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
Session.commit()
|
||||
|
||||
# update mailbox email
|
||||
r = flask_client.put(
|
||||
f"/api/mailboxes/{mb.id}",
|
||||
json={"email": "new-email@gmail.com"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
mb = Mailbox.get(mb.id)
|
||||
assert mb.new_email == "new-email@gmail.com"
|
||||
|
||||
# cancel mailbox email change
|
||||
r = flask_client.put(
|
||||
url_for("api.delete_mailbox", mailbox_id=mb.id),
|
||||
json={"cancel_email_change": True},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
|
||||
mb = Mailbox.get(mb.id)
|
||||
assert mb.new_email is None
|
||||
|
||||
|
||||
def test_get_mailboxes(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
Mailbox.create(user_id=user.id, email="m1@example.com", verified=True)
|
||||
Mailbox.create(user_id=user.id, email="m2@example.com", verified=False)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
"/api/mailboxes",
|
||||
)
|
||||
assert r.status_code == 200
|
||||
# m2@example.com is not returned as it's not verified
|
||||
assert len(r.json["mailboxes"]) == 2
|
||||
|
||||
for mb in r.json["mailboxes"]:
|
||||
assert "email" in mb
|
||||
assert "id" in mb
|
||||
assert "default" in mb
|
||||
assert "creation_timestamp" in mb
|
||||
assert "nb_alias" in mb
|
||||
assert "verified" in mb
|
||||
|
||||
|
||||
def test_get_mailboxes_v2(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
Mailbox.create(user_id=user.id, email="m1@example.com", verified=True)
|
||||
Mailbox.create(user_id=user.id, email="m2@example.com", verified=False)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
"/api/v2/mailboxes",
|
||||
)
|
||||
assert r.status_code == 200
|
||||
# 3 mailboxes: the default, m1 and m2
|
||||
assert len(r.json["mailboxes"]) == 3
|
||||
|
||||
for mb in r.json["mailboxes"]:
|
||||
assert "email" in mb
|
||||
assert "id" in mb
|
||||
assert "default" in mb
|
||||
assert "creation_timestamp" in mb
|
||||
assert "nb_alias" in mb
|
||||
assert "verified" in mb
|
305
app/tests/api/test_new_custom_alias.py
Normal file
305
app/tests/api/test_new_custom_alias.py
Normal file
@ -0,0 +1,305 @@
|
||||
from flask import g
|
||||
|
||||
from app import config
|
||||
from app.alias_suffix import signer
|
||||
from app.alias_utils import delete_alias
|
||||
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
|
||||
from app.db import Session
|
||||
from app.models import Alias, CustomDomain, Mailbox, AliasUsedOn
|
||||
from app.utils import random_word
|
||||
from tests.utils import login, random_domain, random_token
|
||||
|
||||
|
||||
def test_v2(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
signed_suffix = signer.sign(suffix).decode()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v2/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": "prefix",
|
||||
"signed_suffix": signed_suffix,
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"] == f"prefix.{word}@{EMAIL_DOMAIN}"
|
||||
|
||||
res = r.json
|
||||
assert "id" in res
|
||||
assert "email" in res
|
||||
assert "creation_date" in res
|
||||
assert "creation_timestamp" in res
|
||||
assert "nb_forward" in res
|
||||
assert "nb_block" in res
|
||||
assert "nb_reply" in res
|
||||
assert "enabled" in res
|
||||
|
||||
new_alias: Alias = Alias.get_by(email=r.json["alias"])
|
||||
assert len(new_alias.mailboxes) == 1
|
||||
|
||||
|
||||
def test_minimal_payload(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
signed_suffix = signer.sign(suffix).decode()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": "prefix",
|
||||
"signed_suffix": signed_suffix,
|
||||
"mailbox_ids": [user.default_mailbox_id],
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"] == f"prefix.{word}@{EMAIL_DOMAIN}"
|
||||
|
||||
res = r.json
|
||||
assert "id" in res
|
||||
assert "email" in res
|
||||
assert "creation_date" in res
|
||||
assert "creation_timestamp" in res
|
||||
assert "nb_forward" in res
|
||||
assert "nb_block" in res
|
||||
assert "nb_reply" in res
|
||||
assert "enabled" in res
|
||||
|
||||
new_alias: Alias = Alias.get_by(email=r.json["alias"])
|
||||
assert len(new_alias.mailboxes) == 1
|
||||
|
||||
|
||||
def test_full_payload(flask_client):
|
||||
"""Create alias with:
|
||||
- additional mailbox
|
||||
- note
|
||||
- name
|
||||
- hostname (in URL)
|
||||
"""
|
||||
|
||||
user = login(flask_client)
|
||||
|
||||
# create another mailbox
|
||||
mb = Mailbox.create(user_id=user.id, email="abcd@gmail.com", verified=True)
|
||||
Session.commit()
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
signed_suffix = signer.sign(suffix).decode()
|
||||
|
||||
prefix = random_token()
|
||||
|
||||
assert AliasUsedOn.filter(AliasUsedOn.user_id == user.id).count() == 0
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new?hostname=example.com",
|
||||
json={
|
||||
"alias_prefix": prefix,
|
||||
"signed_suffix": signed_suffix,
|
||||
"note": "test note",
|
||||
"mailbox_ids": [user.default_mailbox_id, mb.id],
|
||||
"name": "your name",
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"] == f"{prefix}.{word}@{EMAIL_DOMAIN}"
|
||||
|
||||
# assert returned field
|
||||
res = r.json
|
||||
assert res["note"] == "test note"
|
||||
assert res["name"] == "your name"
|
||||
|
||||
new_alias: Alias = Alias.get_by(email=r.json["alias"])
|
||||
assert new_alias.note == "test note"
|
||||
assert len(new_alias.mailboxes) == 2
|
||||
|
||||
alias_used_on = AliasUsedOn.filter(AliasUsedOn.user_id == user.id).first()
|
||||
assert alias_used_on.alias_id == new_alias.id
|
||||
assert alias_used_on.hostname == "example.com"
|
||||
|
||||
|
||||
def test_custom_domain_alias(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
# create a custom domain
|
||||
domain = random_domain()
|
||||
CustomDomain.create(
|
||||
user_id=user.id, domain=domain, ownership_verified=True, commit=True
|
||||
)
|
||||
|
||||
signed_suffix = signer.sign(f"@{domain}").decode()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": "prefix",
|
||||
"signed_suffix": signed_suffix,
|
||||
"mailbox_ids": [user.default_mailbox_id],
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"] == f"prefix@{domain}"
|
||||
|
||||
|
||||
def test_wrongly_formatted_payload(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json="string isn't a dict",
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {"error": "request body does not follow the required format"}
|
||||
|
||||
|
||||
def test_mailbox_ids_is_not_an_array(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
signed_suffix = signer.sign(suffix).decode()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": "prefix",
|
||||
"signed_suffix": signed_suffix,
|
||||
"mailbox_ids": "not an array",
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {"error": "mailbox_ids must be an array of id"}
|
||||
|
||||
|
||||
def test_out_of_quota(flask_client):
|
||||
user = login(flask_client)
|
||||
user.trial_end = None
|
||||
Session.commit()
|
||||
|
||||
# create MAX_NB_EMAIL_FREE_PLAN custom alias to run out of quota
|
||||
for _ in range(MAX_NB_EMAIL_FREE_PLAN):
|
||||
Alias.create_new(user, prefix="test")
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
signed_suffix = signer.sign(suffix).decode()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": "prefix",
|
||||
"signed_suffix": signed_suffix,
|
||||
"note": "test note",
|
||||
"mailbox_ids": [user.default_mailbox_id],
|
||||
"name": "your name",
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {
|
||||
"error": "You have reached the limitation of a "
|
||||
"free account with the maximum of 3 aliases, please upgrade your plan to create more aliases"
|
||||
}
|
||||
|
||||
|
||||
def test_cannot_create_alias_in_trash(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
# create a custom domain
|
||||
domain = random_domain()
|
||||
CustomDomain.create(
|
||||
user_id=user.id, domain=domain, ownership_verified=True, commit=True
|
||||
)
|
||||
|
||||
signed_suffix = signer.sign(f"@{domain}").decode()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": "prefix",
|
||||
"signed_suffix": signed_suffix,
|
||||
"mailbox_ids": [user.default_mailbox_id],
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"] == f"prefix@{domain}"
|
||||
|
||||
# delete alias: it's going to be moved to domain trash
|
||||
alias = Alias.get_by(email=f"prefix@{domain}")
|
||||
assert alias.custom_domain_id
|
||||
delete_alias(alias, user)
|
||||
|
||||
# try to create the same alias, will fail as the alias is in trash
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": "prefix",
|
||||
"signed_suffix": signed_suffix,
|
||||
"mailbox_ids": [user.default_mailbox_id],
|
||||
},
|
||||
)
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
def test_too_many_requests(flask_client):
|
||||
config.DISABLE_RATE_LIMIT = False
|
||||
|
||||
user = login(flask_client)
|
||||
|
||||
# create a custom domain
|
||||
domain = random_domain()
|
||||
CustomDomain.create(user_id=user.id, domain=domain, verified=True, commit=True)
|
||||
|
||||
# can't create more than 5 aliases in 1 minute
|
||||
for i in range(7):
|
||||
signed_suffix = signer.sign(f"@{domain}").decode()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": f"prefix{i}",
|
||||
"signed_suffix": signed_suffix,
|
||||
"mailbox_ids": [user.default_mailbox_id],
|
||||
},
|
||||
)
|
||||
|
||||
# to make flask-limiter work with unit test
|
||||
# https://github.com/alisaifee/flask-limiter/issues/147#issuecomment-642683820
|
||||
g._rate_limiting_complete = False
|
||||
else:
|
||||
# last request
|
||||
assert r.status_code == 429
|
||||
assert r.json == {"error": "Rate limit exceeded"}
|
||||
|
||||
|
||||
def test_invalid_alias_2_consecutive_dots(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
signed_suffix = signer.sign(suffix).decode()
|
||||
|
||||
r = flask_client.post(
|
||||
"/api/v3/alias/custom/new",
|
||||
json={
|
||||
"alias_prefix": "prefix.", # with the trailing dot, the alias will have 2 consecutive dots
|
||||
"signed_suffix": signed_suffix,
|
||||
"mailbox_ids": [user.default_mailbox_id],
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert r.json == {
|
||||
"error": "2 consecutive dot signs aren't allowed in an email address"
|
||||
}
|
148
app/tests/api/test_new_random_alias.py
Normal file
148
app/tests/api/test_new_random_alias.py
Normal file
@ -0,0 +1,148 @@
|
||||
import uuid
|
||||
|
||||
from flask import url_for, g
|
||||
|
||||
from app import config
|
||||
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
|
||||
from app.db import Session
|
||||
from app.models import Alias, CustomDomain, AliasUsedOn
|
||||
from tests.utils import login, random_domain
|
||||
|
||||
|
||||
def test_with_hostname(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.new_random_alias", hostname="www.test.com"),
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"].endswith("d1.test")
|
||||
|
||||
# make sure alias starts with the suggested prefix
|
||||
assert r.json["alias"].startswith("test")
|
||||
|
||||
# assert returned field
|
||||
res = r.json
|
||||
assert "id" in res
|
||||
assert "email" in res
|
||||
assert "creation_date" in res
|
||||
assert "creation_timestamp" in res
|
||||
assert "nb_forward" in res
|
||||
assert "nb_block" in res
|
||||
assert "nb_reply" in res
|
||||
assert "enabled" in res
|
||||
assert "note" in res
|
||||
|
||||
alias_used_on: AliasUsedOn = AliasUsedOn.order_by(AliasUsedOn.id.desc()).first()
|
||||
assert alias_used_on.hostname == "www.test.com"
|
||||
assert alias_used_on.alias_id == res["id"]
|
||||
|
||||
|
||||
def test_with_custom_domain(flask_client):
|
||||
user = login(flask_client)
|
||||
domain = random_domain()
|
||||
CustomDomain.create(
|
||||
user_id=user.id, domain=domain, ownership_verified=True, commit=True
|
||||
)
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.new_random_alias", hostname="www.test.com"),
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"] == f"test@{domain}"
|
||||
assert Alias.filter_by(user_id=user.id).count() == 2
|
||||
|
||||
# call the endpoint again, should return the same alias
|
||||
r = flask_client.post(
|
||||
url_for("api.new_random_alias", hostname="www.test.com"),
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"] == f"test@{domain}"
|
||||
# no new alias is created
|
||||
assert Alias.filter_by(user_id=user.id).count() == 2
|
||||
|
||||
|
||||
def test_without_hostname(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.new_random_alias"),
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["alias"].endswith(EMAIL_DOMAIN)
|
||||
|
||||
|
||||
def test_custom_mode(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
# without note
|
||||
r = flask_client.post(
|
||||
url_for("api.new_random_alias", mode="uuid"),
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
# extract the uuid part
|
||||
alias = r.json["alias"]
|
||||
uuid_part = alias[: len(alias) - len(EMAIL_DOMAIN) - 1]
|
||||
assert is_valid_uuid(uuid_part)
|
||||
|
||||
# with note
|
||||
r = flask_client.post(
|
||||
url_for("api.new_random_alias", mode="uuid"),
|
||||
json={"note": "test note"},
|
||||
)
|
||||
|
||||
assert r.status_code == 201
|
||||
alias = r.json["alias"]
|
||||
ge = Alias.get_by(email=alias)
|
||||
assert ge.note == "test note"
|
||||
|
||||
|
||||
def test_out_of_quota(flask_client):
|
||||
user = login(flask_client)
|
||||
user.trial_end = None
|
||||
Session.commit()
|
||||
|
||||
# create MAX_NB_EMAIL_FREE_PLAN random alias to run out of quota
|
||||
for _ in range(MAX_NB_EMAIL_FREE_PLAN):
|
||||
Alias.create_new(user, prefix="test1")
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.new_random_alias", hostname="www.test.com"),
|
||||
)
|
||||
|
||||
assert r.status_code == 400
|
||||
assert (
|
||||
r.json["error"] == "You have reached the limitation of a free account with "
|
||||
"the maximum of 3 aliases, please upgrade your plan to create more aliases"
|
||||
)
|
||||
|
||||
|
||||
def test_too_many_requests(flask_client):
|
||||
config.DISABLE_RATE_LIMIT = False
|
||||
login(flask_client)
|
||||
|
||||
# can't create more than 5 aliases in 1 minute
|
||||
for _ in range(7):
|
||||
r = flask_client.post(
|
||||
url_for("api.new_random_alias", hostname="www.test.com", mode="uuid"),
|
||||
)
|
||||
# to make flask-limiter work with unit test
|
||||
# https://github.com/alisaifee/flask-limiter/issues/147#issuecomment-642683820
|
||||
g._rate_limiting_complete = False
|
||||
else:
|
||||
# last request
|
||||
assert r.status_code == 429
|
||||
assert r.json == {"error": "Rate limit exceeded"}
|
||||
|
||||
|
||||
def is_valid_uuid(val):
|
||||
try:
|
||||
uuid.UUID(str(val))
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
53
app/tests/api/test_notification.py
Normal file
53
app/tests/api/test_notification.py
Normal file
@ -0,0 +1,53 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.models import Notification
|
||||
from tests.api.utils import get_new_user_and_api_key
|
||||
|
||||
|
||||
def test_get_notifications(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
# create some notifications
|
||||
Notification.create(user_id=user.id, message="Test message 1")
|
||||
Notification.create(user_id=user.id, message="Test message 2")
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
"/api/notifications?page=0",
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["more"] is False
|
||||
assert len(r.json["notifications"]) == 2
|
||||
for n in r.json["notifications"]:
|
||||
assert n["id"] > 0
|
||||
assert n["message"]
|
||||
assert "title" in n
|
||||
assert n["read"] is False
|
||||
assert n["created_at"]
|
||||
|
||||
# no more post at the next page
|
||||
r = flask_client.get(
|
||||
url_for("api.get_notifications", page=1),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
assert r.json["more"] is False
|
||||
assert len(r.json["notifications"]) == 0
|
||||
|
||||
|
||||
def test_mark_notification_as_read(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
Notification.create(id=1, user_id=user.id, message="Test message 1")
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("api.mark_as_read", notification_id=1),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
notification = Notification.first()
|
||||
assert notification.read
|
64
app/tests/api/test_phone.py
Normal file
64
app/tests/api/test_phone.py
Normal file
@ -0,0 +1,64 @@
|
||||
import arrow
|
||||
|
||||
from app.models import (
|
||||
PhoneReservation,
|
||||
PhoneNumber,
|
||||
PhoneCountry,
|
||||
PhoneMessage,
|
||||
)
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_phone_messages(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
country = PhoneCountry.create(name="FR", commit=True)
|
||||
number = PhoneNumber.create(
|
||||
country_id=country.id, number="+331234567890", active=True, commit=True
|
||||
)
|
||||
reservation = PhoneReservation.create(
|
||||
number_id=number.id,
|
||||
user_id=user.id,
|
||||
start=arrow.now().shift(hours=-1),
|
||||
end=arrow.now().shift(hours=1),
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# no messages yet
|
||||
r = flask_client.post(f"/api/phone/reservations/{reservation.id}")
|
||||
assert r.status_code == 200
|
||||
assert r.json == {"ended": False, "messages": []}
|
||||
|
||||
# a message arrives
|
||||
PhoneMessage.create(
|
||||
number_id=number.id, from_number="from_number", body="body", commit=True
|
||||
)
|
||||
r = flask_client.post(f"/api/phone/reservations/{reservation.id}")
|
||||
assert len(r.json["messages"]) == 1
|
||||
msg = r.json["messages"][0]
|
||||
assert msg["body"] == "body"
|
||||
assert msg["from_number"] == "from_number"
|
||||
assert "created_at" in msg
|
||||
assert "id" in msg
|
||||
|
||||
# print(json.dumps(r.json, indent=2))
|
||||
|
||||
|
||||
def test_phone_messages_ended_reservation(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
country = PhoneCountry.create(name="FR", commit=True)
|
||||
number = PhoneNumber.create(
|
||||
country_id=country.id, number="+331234567890", active=True, commit=True
|
||||
)
|
||||
reservation = PhoneReservation.create(
|
||||
number_id=number.id,
|
||||
user_id=user.id,
|
||||
start=arrow.now().shift(hours=-2),
|
||||
end=arrow.now().shift(hours=-1), # reservation is ended
|
||||
commit=True,
|
||||
)
|
||||
|
||||
r = flask_client.post(f"/api/phone/reservations/{reservation.id}")
|
||||
assert r.status_code == 200
|
||||
assert r.json == {"ended": True, "messages": []}
|
157
app/tests/api/test_serializer.py
Normal file
157
app/tests/api/test_serializer.py
Normal file
@ -0,0 +1,157 @@
|
||||
from app.api.serializer import get_alias_infos_with_pagination_v3
|
||||
from app.config import PAGE_LIMIT
|
||||
from app.db import Session
|
||||
from app.models import Alias, Mailbox, Contact
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def test_get_alias_infos_with_pagination_v3(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
# user has 1 alias that's automatically created when the account is created
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user)
|
||||
assert len(alias_infos) == 1
|
||||
alias_info = alias_infos[0]
|
||||
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
assert alias_info.alias == alias
|
||||
assert alias_info.mailbox == user.default_mailbox
|
||||
assert alias_info.mailboxes == [user.default_mailbox]
|
||||
assert alias_info.nb_forward == 0
|
||||
assert alias_info.nb_blocked == 0
|
||||
assert alias_info.nb_reply == 0
|
||||
assert alias_info.latest_email_log is None
|
||||
assert alias_info.latest_contact is None
|
||||
|
||||
|
||||
def test_get_alias_infos_with_pagination_v3_query_alias_email(flask_client):
|
||||
"""test the query on the alias email"""
|
||||
user = create_new_user()
|
||||
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user, query=alias.email)
|
||||
assert len(alias_infos) == 1
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user, query="no match")
|
||||
assert len(alias_infos) == 0
|
||||
|
||||
|
||||
def test_get_alias_infos_with_pagination_v3_query_alias_mailbox(flask_client):
|
||||
"""test the query on the alias mailbox email"""
|
||||
user = create_new_user()
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user, mailbox_id=alias.mailbox_id)
|
||||
assert len(alias_infos) == 1
|
||||
|
||||
|
||||
def test_get_alias_infos_with_pagination_v3_query_alias_mailboxes(flask_client):
|
||||
"""test the query on the alias additional mailboxes"""
|
||||
user = create_new_user()
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
alias._mailboxes.append(mb)
|
||||
Session.commit()
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user, mailbox_id=mb.id)
|
||||
assert len(alias_infos) == 1
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user, query=alias.email)
|
||||
assert len(alias_infos) == 1
|
||||
|
||||
|
||||
def test_get_alias_infos_with_pagination_v3_query_alias_note(flask_client):
|
||||
"""test the query on the alias note"""
|
||||
user = create_new_user()
|
||||
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
alias.note = "test note"
|
||||
Session.commit()
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user, query="test note")
|
||||
assert len(alias_infos) == 1
|
||||
|
||||
|
||||
def test_get_alias_infos_with_pagination_v3_query_alias_name(flask_client):
|
||||
"""test the query on the alias name"""
|
||||
user = create_new_user()
|
||||
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
alias.name = "Test Name"
|
||||
Session.commit()
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user, query="test name")
|
||||
assert len(alias_infos) == 1
|
||||
|
||||
|
||||
def test_get_alias_infos_with_pagination_v3_no_duplicate(flask_client):
|
||||
"""When an alias belongs to multiple mailboxes, make sure get_alias_infos_with_pagination_v3
|
||||
returns no duplicates
|
||||
"""
|
||||
user = create_new_user()
|
||||
|
||||
alias = Alias.first()
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com")
|
||||
alias._mailboxes.append(mb)
|
||||
Session.commit()
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user)
|
||||
assert len(alias_infos) == 1
|
||||
|
||||
|
||||
def test_get_alias_infos_with_pagination_v3_no_duplicate_when_empty_contact(
|
||||
flask_client,
|
||||
):
|
||||
"""
|
||||
Make sure an alias is returned once when it has 2 contacts that have no email log activity
|
||||
"""
|
||||
user = create_new_user()
|
||||
alias = Alias.first()
|
||||
|
||||
Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
)
|
||||
|
||||
Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact2@example.com",
|
||||
reply_email="rep2@sl.local",
|
||||
)
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user)
|
||||
assert len(alias_infos) == 1
|
||||
|
||||
|
||||
def test_get_alias_infos_pinned_alias(flask_client):
|
||||
"""Different scenarios with pinned alias"""
|
||||
user = create_new_user()
|
||||
|
||||
# to have 3 pages: 2*PAGE_LIMIT + the alias automatically created for a new account
|
||||
for _ in range(2 * PAGE_LIMIT):
|
||||
Alias.create_new_random(user)
|
||||
|
||||
first_alias = Alias.filter_by(user_id=user.id).order_by(Alias.id).first()
|
||||
|
||||
# should return PAGE_LIMIT alias
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user)
|
||||
assert len(alias_infos) == PAGE_LIMIT
|
||||
# make sure first_alias is not returned as the default order is alias creation date
|
||||
assert first_alias not in [ai.alias for ai in alias_infos]
|
||||
|
||||
# pin the first alias
|
||||
first_alias.pinned = True
|
||||
Session.commit()
|
||||
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user)
|
||||
# now first_alias is the first result
|
||||
assert first_alias == alias_infos[0].alias
|
||||
# and the page size is still the same
|
||||
assert len(alias_infos) == PAGE_LIMIT
|
||||
|
||||
# pinned alias isn't included in the search
|
||||
alias_infos = get_alias_infos_with_pagination_v3(user, query="no match")
|
||||
assert len(alias_infos) == 0
|
105
app/tests/api/test_setting.py
Normal file
105
app/tests/api/test_setting.py
Normal file
@ -0,0 +1,105 @@
|
||||
from app.models import (
|
||||
CustomDomain,
|
||||
AliasGeneratorEnum,
|
||||
SenderFormatEnum,
|
||||
AliasSuffixEnum,
|
||||
)
|
||||
from tests.utils import login, random_domain
|
||||
|
||||
|
||||
def test_get_setting(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
r = flask_client.get("/api/setting")
|
||||
assert r.status_code == 200
|
||||
assert r.json == {
|
||||
"alias_generator": "word",
|
||||
"notification": True,
|
||||
"random_alias_default_domain": "sl.local",
|
||||
"sender_format": "AT",
|
||||
"random_alias_suffix": "random_string",
|
||||
}
|
||||
|
||||
|
||||
def test_update_settings_notification(flask_client):
|
||||
user = login(flask_client)
|
||||
assert user.notification
|
||||
|
||||
r = flask_client.patch("/api/setting", json={"notification": False})
|
||||
assert r.status_code == 200
|
||||
assert not user.notification
|
||||
|
||||
|
||||
def test_update_settings_alias_generator(flask_client):
|
||||
user = login(flask_client)
|
||||
assert user.alias_generator == AliasGeneratorEnum.word.value
|
||||
|
||||
r = flask_client.patch("/api/setting", json={"alias_generator": "invalid"})
|
||||
assert r.status_code == 400
|
||||
|
||||
r = flask_client.patch("/api/setting", json={"alias_generator": "uuid"})
|
||||
assert r.status_code == 200
|
||||
assert user.alias_generator == AliasGeneratorEnum.uuid.value
|
||||
|
||||
|
||||
def test_update_settings_random_alias_default_domain(flask_client):
|
||||
user = login(flask_client)
|
||||
assert user.default_random_alias_domain() == "sl.local"
|
||||
|
||||
r = flask_client.patch(
|
||||
"/api/setting", json={"random_alias_default_domain": "invalid"}
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
r = flask_client.patch(
|
||||
"/api/setting", json={"random_alias_default_domain": "d1.test"}
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert user.default_random_alias_domain() == "d1.test"
|
||||
|
||||
|
||||
def test_update_settings_sender_format(flask_client):
|
||||
user = login(flask_client)
|
||||
assert user.sender_format == SenderFormatEnum.AT.value
|
||||
|
||||
r = flask_client.patch("/api/setting", json={"sender_format": "invalid"})
|
||||
assert r.status_code == 400
|
||||
|
||||
r = flask_client.patch("/api/setting", json={"sender_format": "A"})
|
||||
assert r.status_code == 200
|
||||
assert user.sender_format == SenderFormatEnum.A.value
|
||||
|
||||
r = flask_client.patch("/api/setting", json={"sender_format": "NAME_ONLY"})
|
||||
assert r.status_code == 200
|
||||
assert user.sender_format == SenderFormatEnum.NAME_ONLY.value
|
||||
|
||||
|
||||
def test_get_setting_domains(flask_client):
|
||||
user = login(flask_client)
|
||||
domain = random_domain()
|
||||
CustomDomain.create(user_id=user.id, domain=domain, verified=True, commit=True)
|
||||
|
||||
r = flask_client.get("/api/setting/domains")
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_get_setting_domains_v2(flask_client):
|
||||
user = login(flask_client)
|
||||
domain = random_domain()
|
||||
CustomDomain.create(user_id=user.id, domain=domain, verified=True, commit=True)
|
||||
|
||||
r = flask_client.get("/api/v2/setting/domains")
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_update_settings_random_alias_suffix(flask_client):
|
||||
user = login(flask_client)
|
||||
# default random_alias_suffix is random_string
|
||||
assert user.random_alias_suffix == AliasSuffixEnum.random_string.value
|
||||
|
||||
r = flask_client.patch("/api/setting", json={"random_alias_suffix": "invalid"})
|
||||
assert r.status_code == 400
|
||||
|
||||
r = flask_client.patch("/api/setting", json={"random_alias_suffix": "word"})
|
||||
assert r.status_code == 200
|
||||
assert user.random_alias_suffix == AliasSuffixEnum.word.value
|
34
app/tests/api/test_sudo.py
Normal file
34
app/tests/api/test_sudo.py
Normal file
@ -0,0 +1,34 @@
|
||||
from random import random
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from app.api.base import check_sudo_mode_is_active
|
||||
from app.db import Session
|
||||
from app.models import ApiKey
|
||||
from tests.api.utils import get_new_user_and_api_key
|
||||
|
||||
|
||||
def test_enter_sudo_mode(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
password = f"passwd-{random()}"
|
||||
user.set_password(password)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.patch(
|
||||
url_for("api.enter_sudo"),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"password": "invalid"},
|
||||
)
|
||||
|
||||
assert r.status_code == 403
|
||||
assert not check_sudo_mode_is_active(ApiKey.get(id=api_key.id))
|
||||
|
||||
r = flask_client.patch(
|
||||
url_for("api.enter_sudo"),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"password": password},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json == {"ok": True}
|
||||
assert check_sudo_mode_is_active(ApiKey.get(id=api_key.id))
|
68
app/tests/api/test_user.py
Normal file
68
app/tests/api/test_user.py
Normal file
@ -0,0 +1,68 @@
|
||||
from random import random
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from app import config
|
||||
from app.db import Session
|
||||
from app.models import Job, ApiToCookieToken
|
||||
from tests.api.utils import get_new_user_and_api_key
|
||||
|
||||
|
||||
def test_delete_without_sudo(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
for job in Job.all():
|
||||
job.delete(job.id)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.delete(
|
||||
url_for("api.delete_user"),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 440
|
||||
assert Job.count() == 0
|
||||
|
||||
|
||||
def test_delete_with_sudo(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
password = f"passwd-{random()}"
|
||||
user.set_password(password)
|
||||
for job in Job.all():
|
||||
job.delete(job.id)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.patch(
|
||||
url_for("api.enter_sudo"),
|
||||
headers={"Authentication": api_key.code},
|
||||
json={"password": password},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
r = flask_client.delete(
|
||||
url_for("api.delete_user"),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
jobs = Job.all()
|
||||
assert len(jobs) == 1
|
||||
job = jobs[0]
|
||||
assert job.name == config.JOB_DELETE_ACCOUNT
|
||||
assert job.payload == {"user_id": user.id}
|
||||
|
||||
|
||||
def test_get_cookie_token(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.get_api_session_token"),
|
||||
headers={"Authentication": api_key.code},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
code = r.json["token"]
|
||||
token = ApiToCookieToken.get_by(code=code)
|
||||
assert token is not None
|
||||
assert token.user_id == user.id
|
131
app/tests/api/test_user_info.py
Normal file
131
app/tests/api/test_user_info.py
Normal file
@ -0,0 +1,131 @@
|
||||
from flask import url_for
|
||||
|
||||
from app import config
|
||||
from app.models import User, PartnerUser
|
||||
from app.proton.utils import get_proton_partner
|
||||
from tests.api.utils import get_new_user_and_api_key
|
||||
from tests.utils import login, random_token, random_email
|
||||
|
||||
|
||||
def test_user_in_trial(flask_client):
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.user_info"), headers={"Authentication": api_key.code}
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json == {
|
||||
"is_premium": True,
|
||||
"name": "Test User",
|
||||
"email": user.email,
|
||||
"in_trial": True,
|
||||
"profile_picture_url": None,
|
||||
"max_alias_free_plan": config.MAX_NB_EMAIL_FREE_PLAN,
|
||||
"connected_proton_address": None,
|
||||
}
|
||||
|
||||
|
||||
def test_user_linked_to_proton(flask_client):
|
||||
config.CONNECT_WITH_PROTON = True
|
||||
user, api_key = get_new_user_and_api_key()
|
||||
partner = get_proton_partner()
|
||||
partner_email = random_email()
|
||||
PartnerUser.create(
|
||||
user_id=user.id,
|
||||
partner_id=partner.id,
|
||||
external_user_id=random_token(),
|
||||
partner_email=partner_email,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("api.user_info"), headers={"Authentication": api_key.code}
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json == {
|
||||
"is_premium": True,
|
||||
"name": "Test User",
|
||||
"email": user.email,
|
||||
"in_trial": True,
|
||||
"profile_picture_url": None,
|
||||
"max_alias_free_plan": config.MAX_NB_EMAIL_FREE_PLAN,
|
||||
"connected_proton_address": partner_email,
|
||||
}
|
||||
|
||||
|
||||
def test_wrong_api_key(flask_client):
|
||||
r = flask_client.get(
|
||||
url_for("api.user_info"), headers={"Authentication": "Invalid code"}
|
||||
)
|
||||
|
||||
assert r.status_code == 401
|
||||
|
||||
assert r.json == {"error": "Wrong api key"}
|
||||
|
||||
|
||||
def test_create_api_key(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
# create api key
|
||||
r = flask_client.post(url_for("api.create_api_key"), json={"device": "Test device"})
|
||||
|
||||
assert r.status_code == 201
|
||||
assert r.json["api_key"]
|
||||
|
||||
|
||||
def test_logout(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
# logout
|
||||
r = flask_client.get(
|
||||
url_for("auth.logout"),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_change_profile_picture(flask_client):
|
||||
user = login(flask_client)
|
||||
assert not user.profile_picture_id
|
||||
|
||||
# <<< Set the profile picture >>>
|
||||
img_base64 = """iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="""
|
||||
r = flask_client.patch(
|
||||
"/api/user_info",
|
||||
json={"profile_picture": img_base64},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["profile_picture_url"] is not None
|
||||
|
||||
user = User.get(user.id)
|
||||
assert user.profile_picture_id
|
||||
|
||||
# <<< remove the profile picture >>>
|
||||
r = flask_client.patch(
|
||||
"/api/user_info",
|
||||
json={"profile_picture": None},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json["profile_picture_url"] is None
|
||||
|
||||
user = User.get(user.id)
|
||||
assert not user.profile_picture_id
|
||||
|
||||
|
||||
def test_change_name(flask_client):
|
||||
user = login(flask_client)
|
||||
assert user.name != "new name"
|
||||
|
||||
r = flask_client.patch(
|
||||
"/api/user_info",
|
||||
json={"name": "new name"},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json["name"] == "new name"
|
||||
|
||||
assert user.name == "new name"
|
13
app/tests/api/utils.py
Normal file
13
app/tests/api/utils.py
Normal file
@ -0,0 +1,13 @@
|
||||
from typing import Tuple
|
||||
|
||||
from app.models import User, ApiKey
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def get_new_user_and_api_key() -> Tuple[User, ApiKey]:
|
||||
user = create_new_user()
|
||||
|
||||
# create api_key
|
||||
api_key = ApiKey.create(user.id, "for test", commit=True)
|
||||
|
||||
return user, api_key
|
0
app/tests/auth/__init__.py
Normal file
0
app/tests/auth/__init__.py
Normal file
29
app/tests/auth/test_api_to_cookie.py
Normal file
29
app/tests/auth/test_api_to_cookie.py
Normal file
@ -0,0 +1,29 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.models import ApiToCookieToken, ApiKey
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def test_get_cookie(flask_client):
|
||||
user = create_new_user()
|
||||
api_key = ApiKey.create(
|
||||
user_id=user.id,
|
||||
commit=True,
|
||||
)
|
||||
token = ApiToCookieToken.create(
|
||||
user_id=user.id,
|
||||
api_key_id=api_key.id,
|
||||
commit=True,
|
||||
)
|
||||
token_code = token.code
|
||||
token_id = token.id
|
||||
|
||||
r = flask_client.get(
|
||||
url_for(
|
||||
"auth.api_to_cookie", token=token_code, next=url_for("dashboard.setting")
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert ApiToCookieToken.get(token_id) is None
|
||||
assert r.headers.getlist("Set-Cookie") is not None
|
33
app/tests/auth/test_change_email.py
Normal file
33
app/tests/auth/test_change_email.py
Normal file
@ -0,0 +1,33 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.models import EmailChange, User, ResetPasswordCode
|
||||
from tests.utils import create_new_user, random_token, random_email
|
||||
|
||||
|
||||
def test_change_email(flask_client):
|
||||
user = create_new_user()
|
||||
user.activated = False
|
||||
user_id = user.id
|
||||
email_change = EmailChange.create(
|
||||
user_id=user.id,
|
||||
code=random_token(),
|
||||
new_email=random_email(),
|
||||
)
|
||||
reset_id = ResetPasswordCode.create(user_id=user_id, code=random_token()).id
|
||||
email_change_id = email_change.id
|
||||
email_change_code = email_change.code
|
||||
new_email = email_change.new_email
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("auth.change_email", code=email_change_code),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
|
||||
user = User.get(user_id)
|
||||
assert user.email == new_email
|
||||
assert EmailChange.get(email_change_id) is None
|
||||
assert ResetPasswordCode.get(reset_id) is None
|
82
app/tests/auth/test_login.py
Normal file
82
app/tests/auth/test_login.py
Normal file
@ -0,0 +1,82 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.utils import canonicalize_email, random_string
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def test_unactivated_user_login(flask_client):
|
||||
user = create_new_user()
|
||||
user.activated = False
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.login"),
|
||||
data={"email": user.email, "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
b"Please check your inbox for the activation email. You can also have this email re-sent"
|
||||
in r.data
|
||||
)
|
||||
|
||||
|
||||
def test_non_canonical_login(flask_client):
|
||||
email = f"pre.{random_string(10)}@gmail.com"
|
||||
name = f"NAME-{random_string(10)}"
|
||||
user = create_new_user(email, name)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.login"),
|
||||
data={"email": user.email, "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert name.encode("utf-8") in r.data
|
||||
|
||||
canonical_email = canonicalize_email(email)
|
||||
assert canonical_email != email
|
||||
|
||||
flask_client.get(url_for("auth.logout"))
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.login"),
|
||||
data={"email": canonical_email, "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert name.encode("utf-8") not in r.data
|
||||
|
||||
|
||||
def test_canonical_login_with_non_canonical_email(flask_client):
|
||||
suffix = f"{random_string(10)}@gmail.com"
|
||||
canonical_email = f"pre{suffix}"
|
||||
non_canonical_email = f"pre.{suffix}"
|
||||
name = f"NAME-{random_string(10)}"
|
||||
create_new_user(canonical_email, name)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.login"),
|
||||
data={"email": non_canonical_email, "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert name.encode("utf-8") in r.data
|
||||
|
||||
flask_client.get(url_for("auth.logout"))
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.login"),
|
||||
data={"email": canonical_email, "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert name.encode("utf-8") in r.data
|
23
app/tests/auth/test_proton.py
Normal file
23
app/tests/auth/test_proton.py
Normal file
@ -0,0 +1,23 @@
|
||||
from flask import url_for
|
||||
from urllib.parse import parse_qs
|
||||
from urllib3.util import parse_url
|
||||
|
||||
from app.config import URL, PROTON_CLIENT_ID
|
||||
|
||||
|
||||
def test_login_with_proton(flask_client):
|
||||
r = flask_client.get(
|
||||
url_for("auth.proton_login"),
|
||||
follow_redirects=False,
|
||||
)
|
||||
location = r.headers.get("Location")
|
||||
assert location is not None
|
||||
|
||||
parsed = parse_url(location)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
expected_redirect_url = f"{URL}/auth/proton/callback"
|
||||
|
||||
assert "code" == query["response_type"][0]
|
||||
assert PROTON_CLIENT_ID == query["client_id"][0]
|
||||
assert expected_redirect_url == query["redirect_uri"][0]
|
88
app/tests/auth/test_register.py
Normal file
88
app/tests/auth/test_register.py
Normal file
@ -0,0 +1,88 @@
|
||||
from flask import url_for
|
||||
|
||||
from app import config
|
||||
from app.db import Session
|
||||
from app.models import DailyMetric, User
|
||||
from app.utils import canonicalize_email
|
||||
from tests.utils import create_new_user, random_email
|
||||
|
||||
|
||||
def setup_module():
|
||||
config.SKIP_MX_LOOKUP_ON_CHECK = True
|
||||
|
||||
|
||||
def teardown_module():
|
||||
config.SKIP_MX_LOOKUP_ON_CHECK = False
|
||||
|
||||
|
||||
def test_register_success(flask_client):
|
||||
email = random_email()
|
||||
r = flask_client.post(
|
||||
url_for("auth.register"),
|
||||
data={"email": email, "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
# User arrives at the waiting activation page.
|
||||
assert b"An email to validate your email is on its way" in r.data
|
||||
|
||||
|
||||
def test_register_increment_nb_new_web_non_proton_user(flask_client):
|
||||
daily_metric = DailyMetric.get_or_create_today_metric()
|
||||
Session.commit()
|
||||
nb_new_web_non_proton_user = daily_metric.nb_new_web_non_proton_user
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.register"),
|
||||
data={"email": random_email(), "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
new_daily_metric = DailyMetric.get_or_create_today_metric()
|
||||
assert new_daily_metric.nb_new_web_non_proton_user == nb_new_web_non_proton_user + 1
|
||||
|
||||
|
||||
def test_register_disabled(flask_client):
|
||||
"""User cannot create new account when DISABLE_REGISTRATION."""
|
||||
|
||||
config.DISABLE_REGISTRATION = True
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.register"),
|
||||
data={"email": "abcd@gmail.com", "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
config.DISABLE_REGISTRATION = False
|
||||
assert b"Registration is closed" in r.data
|
||||
|
||||
|
||||
def test_register_non_canonical_if_canonical_exists_is_not_allowed(flask_client):
|
||||
"""User cannot create new account if the canonical name clashes"""
|
||||
email = f"noncan.{random_email()}"
|
||||
canonical_email = canonicalize_email(email)
|
||||
create_new_user(email=canonical_email)
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.register"),
|
||||
data={"email": email, "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert f"Email {canonical_email} already used".encode("utf-8") in r.data
|
||||
|
||||
|
||||
def test_register_non_canonical_is_canonicalized(flask_client):
|
||||
"""User cannot create new account if the canonical name clashes"""
|
||||
email = f"noncan.{random_email()}"
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("auth.register"),
|
||||
data={"email": email, "password": "password"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert b"An email to validate your email is on its way" in r.data
|
||||
assert User.get_by(email=canonicalize_email(email)) is not None
|
64
app/tests/conftest.py
Normal file
64
app/tests/conftest.py
Normal file
@ -0,0 +1,64 @@
|
||||
import os
|
||||
|
||||
# use the tests/test.env config fle
|
||||
# flake8: noqa: E402
|
||||
|
||||
os.environ["CONFIG"] = os.path.abspath(
|
||||
os.path.join(os.path.dirname(os.path.dirname(__file__)), "tests/test.env")
|
||||
)
|
||||
import sqlalchemy
|
||||
|
||||
from app.db import Session, engine, connection
|
||||
|
||||
from psycopg2 import errors
|
||||
from psycopg2.errorcodes import DEPENDENT_OBJECTS_STILL_EXIST
|
||||
|
||||
import pytest
|
||||
|
||||
from server import create_app
|
||||
from init_app import add_sl_domains, add_proton_partner
|
||||
|
||||
app = create_app()
|
||||
app.config["TESTING"] = True
|
||||
app.config["WTF_CSRF_ENABLED"] = False
|
||||
app.config["SERVER_NAME"] = "sl.test"
|
||||
|
||||
# enable pg_trgm extension
|
||||
with engine.connect() as conn:
|
||||
try:
|
||||
conn.execute("DROP EXTENSION if exists pg_trgm")
|
||||
conn.execute("CREATE EXTENSION pg_trgm")
|
||||
except sqlalchemy.exc.InternalError as e:
|
||||
if isinstance(e.orig, errors.lookup(DEPENDENT_OBJECTS_STILL_EXIST)):
|
||||
print(">>> pg_trgm can't be dropped, ignore")
|
||||
conn.execute("Rollback")
|
||||
|
||||
add_sl_domains()
|
||||
add_proton_partner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flask_app():
|
||||
yield app
|
||||
|
||||
|
||||
from app import config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def flask_client():
|
||||
transaction = connection.begin()
|
||||
|
||||
with app.app_context():
|
||||
# disable rate limit during test
|
||||
config.DISABLE_RATE_LIMIT = True
|
||||
try:
|
||||
client = app.test_client()
|
||||
yield client
|
||||
finally:
|
||||
# disable rate limit again as some tests might enable rate limit
|
||||
config.DISABLE_RATE_LIMIT = True
|
||||
# roll back all commits made during a test
|
||||
transaction.rollback()
|
||||
Session.rollback()
|
||||
Session.close()
|
0
app/tests/dashboard/__init__.py
Normal file
0
app/tests/dashboard/__init__.py
Normal file
59
app/tests/dashboard/test_alias_contact_manager.py
Normal file
59
app/tests/dashboard/test_alias_contact_manager.py
Normal file
@ -0,0 +1,59 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.models import (
|
||||
Alias,
|
||||
Contact,
|
||||
)
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_add_contact_success(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.filter(Alias.user_id == user.id).first()
|
||||
|
||||
assert Contact.filter_by(user_id=user.id).count() == 0
|
||||
|
||||
# <<< Create a new contact >>>
|
||||
flask_client.post(
|
||||
url_for("dashboard.alias_contact_manager", alias_id=alias.id),
|
||||
data={
|
||||
"form-name": "create",
|
||||
"email": "abcd@gmail.com",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
# a new contact is added
|
||||
assert Contact.filter_by(user_id=user.id).count() == 1
|
||||
contact = Contact.filter_by(user_id=user.id).first()
|
||||
assert contact.website_email == "abcd@gmail.com"
|
||||
|
||||
# <<< Create a new contact using a full email format >>>
|
||||
flask_client.post(
|
||||
url_for("dashboard.alias_contact_manager", alias_id=alias.id),
|
||||
data={
|
||||
"form-name": "create",
|
||||
"email": "First Last <another@gmail.com>",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
# a new contact is added
|
||||
assert Contact.filter_by(user_id=user.id).count() == 2
|
||||
contact = (
|
||||
Contact.filter_by(user_id=user.id).filter(Contact.id != contact.id).first()
|
||||
)
|
||||
assert contact.website_email == "another@gmail.com"
|
||||
assert contact.name == "First Last"
|
||||
|
||||
# <<< Create a new contact with invalid email address >>>
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.alias_contact_manager", alias_id=alias.id),
|
||||
data={
|
||||
"form-name": "create",
|
||||
"email": "with space@gmail.com",
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
# no new contact is added
|
||||
assert Contact.filter_by(user_id=user.id).count() == 2
|
||||
assert "Invalid email format. Email must be either email@example.com" in str(r.data)
|
5
app/tests/dashboard/test_alias_csv_export.py
Normal file
5
app/tests/dashboard/test_alias_csv_export.py
Normal file
@ -0,0 +1,5 @@
|
||||
from tests.utils_test_alias import alias_export
|
||||
|
||||
|
||||
def test_alias_export(flask_client):
|
||||
alias_export(flask_client, "dashboard.alias_export_route")
|
38
app/tests/dashboard/test_alias_transfer.py
Normal file
38
app/tests/dashboard/test_alias_transfer.py
Normal file
@ -0,0 +1,38 @@
|
||||
from app.dashboard.views import alias_transfer
|
||||
from app.db import Session
|
||||
from app.models import (
|
||||
Alias,
|
||||
Mailbox,
|
||||
User,
|
||||
AliasMailbox,
|
||||
)
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_alias_transfer(flask_client):
|
||||
user = login(flask_client)
|
||||
mb = Mailbox.create(user_id=user.id, email="mb@gmail.com", commit=True)
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
AliasMailbox.create(alias_id=alias.id, mailbox_id=mb.id, commit=True)
|
||||
|
||||
new_user = User.create(
|
||||
email="hey@example.com",
|
||||
password="password",
|
||||
activated=True,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
Mailbox.create(
|
||||
user_id=new_user.id, email="hey2@example.com", verified=True, commit=True
|
||||
)
|
||||
|
||||
alias_transfer.transfer(alias, new_user, new_user.mailboxes())
|
||||
|
||||
# refresh from db
|
||||
alias = Alias.get(alias.id)
|
||||
assert alias.user == new_user
|
||||
assert set(alias.mailboxes) == set(new_user.mailboxes())
|
||||
assert len(alias.mailboxes) == 2
|
89
app/tests/dashboard/test_api_keys.py
Normal file
89
app/tests/dashboard/test_api_keys.py
Normal file
@ -0,0 +1,89 @@
|
||||
from time import time
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.models import User, ApiKey
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_api_key_page_requires_password(flask_client):
|
||||
r = flask_client.get(
|
||||
url_for("dashboard.api_key"),
|
||||
)
|
||||
|
||||
assert r.status_code == 302
|
||||
|
||||
|
||||
def test_create_delete_api_key(flask_client):
|
||||
user = login(flask_client)
|
||||
nb_api_key = ApiKey.count()
|
||||
|
||||
# to bypass sudo mode
|
||||
with flask_client.session_transaction() as session:
|
||||
session["sudo_time"] = int(time())
|
||||
|
||||
# create api_key
|
||||
create_r = flask_client.post(
|
||||
url_for("dashboard.api_key"),
|
||||
data={"form-name": "create", "name": "for test"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert create_r.status_code == 200
|
||||
api_key = ApiKey.get_by(user_id=user.id)
|
||||
assert ApiKey.filter(ApiKey.user_id == user.id).count() == 1
|
||||
assert api_key.name == "for test"
|
||||
|
||||
# delete api_key
|
||||
delete_r = flask_client.post(
|
||||
url_for("dashboard.api_key"),
|
||||
data={"form-name": "delete", "api-key-id": api_key.id},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert delete_r.status_code == 200
|
||||
assert ApiKey.count() == nb_api_key
|
||||
|
||||
|
||||
def test_delete_all_api_keys(flask_client):
|
||||
nb_api_keys = ApiKey.count()
|
||||
|
||||
# create two test users
|
||||
user_1 = login(flask_client)
|
||||
user_2 = User.create(
|
||||
email="a2@b.c", password="password", name="Test User 2", activated=True
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
# create api_key for both users
|
||||
ApiKey.create(user_1.id, "for test")
|
||||
ApiKey.create(user_1.id, "for test 2")
|
||||
ApiKey.create(user_2.id, "for test")
|
||||
Session.commit()
|
||||
|
||||
assert (
|
||||
ApiKey.count() == nb_api_keys + 3
|
||||
) # assert that the total number of API keys for all users is 3.
|
||||
# assert that each user has the API keys created
|
||||
assert ApiKey.filter(ApiKey.user_id == user_1.id).count() == 2
|
||||
assert ApiKey.filter(ApiKey.user_id == user_2.id).count() == 1
|
||||
|
||||
# to bypass sudo mode
|
||||
with flask_client.session_transaction() as session:
|
||||
session["sudo_time"] = int(time())
|
||||
|
||||
# delete all of user 1's API keys
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.api_key"),
|
||||
data={"form-name": "delete-all"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
ApiKey.count() == nb_api_keys + 1
|
||||
) # assert that the total number of API keys for all users is now 1.
|
||||
assert (
|
||||
ApiKey.filter(ApiKey.user_id == user_1.id).count() == 0
|
||||
) # assert that user 1 now has 0 API keys
|
||||
assert (
|
||||
ApiKey.filter(ApiKey.user_id == user_2.id).count() == 1
|
||||
) # assert that user 2 still has 1 API key
|
391
app/tests/dashboard/test_custom_alias.py
Normal file
391
app/tests/dashboard/test_custom_alias.py
Normal file
@ -0,0 +1,391 @@
|
||||
from random import random
|
||||
|
||||
from flask import url_for, g
|
||||
|
||||
from app import config
|
||||
from app.alias_suffix import (
|
||||
get_alias_suffixes,
|
||||
AliasSuffix,
|
||||
signer,
|
||||
verify_prefix_suffix,
|
||||
)
|
||||
from app.alias_utils import delete_alias
|
||||
from app.config import EMAIL_DOMAIN
|
||||
from app.db import Session
|
||||
from app.models import (
|
||||
Mailbox,
|
||||
CustomDomain,
|
||||
Alias,
|
||||
DomainDeletedAlias,
|
||||
DeletedAlias,
|
||||
SLDomain,
|
||||
DailyMetric,
|
||||
)
|
||||
from app.utils import random_word
|
||||
from tests.utils import login, random_domain, create_new_user
|
||||
|
||||
|
||||
def test_add_alias_success(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
suffix = f".{int(random() * 100000)}@{EMAIL_DOMAIN}"
|
||||
alias_suffix = AliasSuffix(
|
||||
is_custom=False,
|
||||
suffix=suffix,
|
||||
signed_suffix=signer.sign(suffix).decode(),
|
||||
is_premium=False,
|
||||
domain=EMAIL_DOMAIN,
|
||||
)
|
||||
|
||||
# create with a single mailbox
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_alias"),
|
||||
data={
|
||||
"prefix": "prefix",
|
||||
"signed-alias-suffix": alias_suffix.signed_suffix,
|
||||
"mailboxes": [user.default_mailbox_id],
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert f"Alias prefix{alias_suffix.suffix} has been created" in str(r.data)
|
||||
|
||||
alias = Alias.order_by(Alias.created_at.desc()).first()
|
||||
assert not alias._mailboxes
|
||||
|
||||
|
||||
def test_add_alias_increment_nb_daily_metric_alias(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
daily_metric = DailyMetric.get_or_create_today_metric()
|
||||
Session.commit()
|
||||
nb_alias = daily_metric.nb_alias
|
||||
|
||||
suffix = f".{int(random() * 100000)}@{EMAIL_DOMAIN}"
|
||||
alias_suffix = AliasSuffix(
|
||||
is_custom=False,
|
||||
suffix=suffix,
|
||||
signed_suffix=signer.sign(suffix).decode(),
|
||||
is_premium=False,
|
||||
domain=EMAIL_DOMAIN,
|
||||
)
|
||||
|
||||
# create with a single mailbox
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_alias"),
|
||||
data={
|
||||
"prefix": "prefix",
|
||||
"signed-alias-suffix": alias_suffix.signed_suffix,
|
||||
"mailboxes": [user.default_mailbox_id],
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
new_daily_metric = DailyMetric.get_or_create_today_metric()
|
||||
assert new_daily_metric.nb_alias == nb_alias + 1
|
||||
|
||||
|
||||
def test_add_alias_multiple_mailboxes(flask_client):
|
||||
user = login(flask_client)
|
||||
Session.commit()
|
||||
|
||||
suffix = f".{int(random() * 100000)}@{EMAIL_DOMAIN}"
|
||||
alias_suffix = AliasSuffix(
|
||||
is_custom=False,
|
||||
suffix=suffix,
|
||||
signed_suffix=signer.sign(suffix).decode(),
|
||||
is_premium=False,
|
||||
domain=EMAIL_DOMAIN,
|
||||
)
|
||||
|
||||
# create with a multiple mailboxes
|
||||
mb1 = Mailbox.create(user_id=user.id, email="m1@example.com", verified=True)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_alias"),
|
||||
data={
|
||||
"prefix": "prefix",
|
||||
"signed-alias-suffix": alias_suffix.signed_suffix,
|
||||
"mailboxes": [user.default_mailbox_id, mb1.id],
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert f"Alias prefix{alias_suffix.suffix} has been created" in str(r.data)
|
||||
|
||||
alias = Alias.order_by(Alias.created_at.desc()).first()
|
||||
assert alias._mailboxes
|
||||
|
||||
|
||||
def test_not_show_unverified_mailbox(flask_client):
|
||||
"""make sure user unverified mailbox is not shown to user"""
|
||||
user = login(flask_client)
|
||||
Session.commit()
|
||||
|
||||
Mailbox.create(user_id=user.id, email="m1@example.com", verified=True)
|
||||
Mailbox.create(user_id=user.id, email="m2@example.com", verified=False)
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(url_for("dashboard.custom_alias"))
|
||||
|
||||
assert "m1@example.com" in str(r.data)
|
||||
assert "m2@example.com" not in str(r.data)
|
||||
|
||||
|
||||
def test_verify_prefix_suffix(flask_client):
|
||||
user = login(flask_client)
|
||||
Session.commit()
|
||||
|
||||
CustomDomain.create(user_id=user.id, domain="test.com", ownership_verified=True)
|
||||
|
||||
assert verify_prefix_suffix(user, "prefix", "@test.com")
|
||||
assert not verify_prefix_suffix(user, "prefix", "@abcd.com")
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
assert verify_prefix_suffix(user, "prefix", suffix)
|
||||
|
||||
|
||||
def test_available_suffixes(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
CustomDomain.create(user_id=user.id, domain="test.com", ownership_verified=True)
|
||||
|
||||
assert len(get_alias_suffixes(user)) > 0
|
||||
|
||||
# first suffix is custom domain
|
||||
first_suffix = get_alias_suffixes(user)[0]
|
||||
assert first_suffix.is_custom
|
||||
assert first_suffix.suffix == "@test.com"
|
||||
assert first_suffix.signed_suffix.startswith("@test.com")
|
||||
|
||||
|
||||
def test_available_suffixes_default_domain(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
sl_domain = SLDomain.first()
|
||||
CustomDomain.create(
|
||||
user_id=user.id, domain="test.com", ownership_verified=True, commit=True
|
||||
)
|
||||
|
||||
user.default_alias_public_domain_id = sl_domain.id
|
||||
|
||||
# first suffix is SL Domain
|
||||
first_suffix = get_alias_suffixes(user)[0]
|
||||
assert first_suffix.suffix.endswith(f"@{sl_domain.domain}")
|
||||
|
||||
user.default_alias_public_domain_id = None
|
||||
# first suffix is custom domain
|
||||
first_suffix = get_alias_suffixes(user)[0]
|
||||
assert first_suffix.suffix == "@test.com"
|
||||
|
||||
|
||||
def test_available_suffixes_random_prefix_generation(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
CustomDomain.create(
|
||||
user_id=user.id, domain="test.com", ownership_verified=True, commit=True
|
||||
)
|
||||
cd2 = CustomDomain.create(
|
||||
user_id=user.id, domain="test2.com", ownership_verified=True, commit=True
|
||||
)
|
||||
|
||||
user.default_alias_custom_domain_id = cd2.id
|
||||
|
||||
# first suffix is test2.com
|
||||
first_suffix = get_alias_suffixes(user)[0]
|
||||
assert first_suffix.suffix == "@test2.com"
|
||||
|
||||
cd2.random_prefix_generation = True
|
||||
# e.g. .meo@test2.com
|
||||
first_suffix = get_alias_suffixes(user)[0]
|
||||
assert first_suffix.suffix.endswith("@test2.com")
|
||||
assert first_suffix.suffix.startswith(".")
|
||||
|
||||
|
||||
def test_available_suffixes_hidden_domain(flask_client):
|
||||
user = login(flask_client)
|
||||
nb_suffix = len(get_alias_suffixes(user))
|
||||
|
||||
sl_domain = SLDomain.create(domain=random_domain(), commit=True)
|
||||
assert len(get_alias_suffixes(user)) == nb_suffix + 1
|
||||
|
||||
sl_domain.hidden = True
|
||||
Session.commit()
|
||||
assert len(get_alias_suffixes(user)) == nb_suffix
|
||||
|
||||
|
||||
def test_available_suffixes_domain_order(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
domain = random_domain()
|
||||
# will be the last domain as other domains have order=0
|
||||
sl_domain = SLDomain.create(domain=domain, order=1, commit=True)
|
||||
last_suffix_info = get_alias_suffixes(user)[-1]
|
||||
assert last_suffix_info.suffix.endswith(domain)
|
||||
|
||||
# now will be the first domain
|
||||
sl_domain.order = -1
|
||||
Session.commit()
|
||||
first_suffix_info = get_alias_suffixes(user)[0]
|
||||
assert first_suffix_info.suffix.endswith(domain)
|
||||
|
||||
|
||||
def test_add_already_existed_alias(flask_client):
|
||||
user = login(flask_client)
|
||||
Session.commit()
|
||||
|
||||
another_user = create_new_user()
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
|
||||
alias_suffix = AliasSuffix(
|
||||
is_custom=False,
|
||||
suffix=suffix,
|
||||
signed_suffix=signer.sign(suffix).decode(),
|
||||
is_premium=False,
|
||||
domain=EMAIL_DOMAIN,
|
||||
)
|
||||
|
||||
# alias already exist
|
||||
Alias.create(
|
||||
user_id=another_user.id,
|
||||
email=f"prefix{suffix}",
|
||||
mailbox_id=another_user.default_mailbox_id,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# create the same alias, should return error
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_alias"),
|
||||
data={
|
||||
"prefix": "prefix",
|
||||
"signed-alias-suffix": alias_suffix.signed_suffix,
|
||||
"mailboxes": [user.default_mailbox_id],
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert f"prefix{suffix} cannot be used" in r.get_data(True)
|
||||
|
||||
|
||||
def test_add_alias_in_global_trash(flask_client):
|
||||
user = login(flask_client)
|
||||
Session.commit()
|
||||
|
||||
another_user = create_new_user()
|
||||
|
||||
word = random_word()
|
||||
suffix = f".{word}@{EMAIL_DOMAIN}"
|
||||
alias_suffix = AliasSuffix(
|
||||
is_custom=False,
|
||||
suffix=suffix,
|
||||
signed_suffix=signer.sign(suffix).decode(),
|
||||
is_premium=False,
|
||||
domain=EMAIL_DOMAIN,
|
||||
)
|
||||
|
||||
# delete an alias: alias should go the DeletedAlias
|
||||
alias = Alias.create(
|
||||
user_id=another_user.id,
|
||||
email=f"prefix{suffix}",
|
||||
mailbox_id=another_user.default_mailbox_id,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
prev_deleted = DeletedAlias.count()
|
||||
delete_alias(alias, another_user)
|
||||
assert prev_deleted + 1 == DeletedAlias.count()
|
||||
|
||||
# create the same alias, should return error
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_alias"),
|
||||
data={
|
||||
"prefix": "prefix",
|
||||
"signed-alias-suffix": alias_suffix.signed_suffix,
|
||||
"mailboxes": [user.default_mailbox_id],
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert f"prefix{suffix} cannot be used" in r.get_data(True)
|
||||
|
||||
|
||||
def test_add_alias_in_custom_domain_trash(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
domain = random_domain()
|
||||
custom_domain = CustomDomain.create(
|
||||
user_id=user.id, domain=domain, ownership_verified=True, commit=True
|
||||
)
|
||||
|
||||
# delete a custom-domain alias: alias should go the DomainDeletedAlias
|
||||
alias = Alias.create(
|
||||
user_id=user.id,
|
||||
email=f"prefix@{domain}",
|
||||
custom_domain_id=custom_domain.id,
|
||||
mailbox_id=user.default_mailbox_id,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert DomainDeletedAlias.count() == 0
|
||||
delete_alias(alias, user)
|
||||
assert DomainDeletedAlias.count() == 1
|
||||
|
||||
# create the same alias, should return error
|
||||
suffix = f"@{domain}"
|
||||
|
||||
alias_suffix = AliasSuffix(
|
||||
is_custom=False,
|
||||
suffix=suffix,
|
||||
signed_suffix=signer.sign(suffix).decode(),
|
||||
is_premium=False,
|
||||
domain=EMAIL_DOMAIN,
|
||||
)
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_alias"),
|
||||
data={
|
||||
"prefix": "prefix",
|
||||
"signed-alias-suffix": alias_suffix.signed_suffix,
|
||||
"mailboxes": [user.default_mailbox_id],
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert "You have deleted this alias before. You can restore it on" in r.get_data(
|
||||
True
|
||||
)
|
||||
|
||||
|
||||
def test_too_many_requests(flask_client):
|
||||
config.DISABLE_RATE_LIMIT = False
|
||||
user = login(flask_client)
|
||||
|
||||
# create a custom domain
|
||||
domain = random_domain()
|
||||
CustomDomain.create(user_id=user.id, domain=domain, verified=True, commit=True)
|
||||
|
||||
# can't create more than 5 aliases in 1 minute
|
||||
for i in range(7):
|
||||
signed_suffix = signer.sign(f"@{domain}").decode()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_alias"),
|
||||
data={
|
||||
"prefix": f"prefix{i}",
|
||||
"suffix": signed_suffix,
|
||||
"mailboxes": [user.default_mailbox_id],
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
# to make flask-limiter work with unit test
|
||||
# https://github.com/alisaifee/flask-limiter/issues/147#issuecomment-642683820
|
||||
g._rate_limiting_complete = False
|
||||
else:
|
||||
# last request
|
||||
assert r.status_code == 429
|
||||
assert "Whoa, slow down there, pardner!" in str(r.data)
|
61
app/tests/dashboard/test_custom_domain.py
Normal file
61
app/tests/dashboard/test_custom_domain.py
Normal file
@ -0,0 +1,61 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.email_utils import get_email_domain_part
|
||||
from app.models import Mailbox
|
||||
from tests.utils import login, random_domain
|
||||
|
||||
|
||||
def test_add_domain_success(flask_client):
|
||||
user = login(flask_client)
|
||||
user.lifetime = True
|
||||
Session.commit()
|
||||
|
||||
domain = random_domain()
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_domain"),
|
||||
data={"form-name": "create", "domain": domain},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert f"New domain {domain} is created".encode() in r.data
|
||||
|
||||
|
||||
def test_add_domain_same_as_user_email(flask_client):
|
||||
"""cannot add domain if user personal email uses this domain"""
|
||||
user = login(flask_client)
|
||||
user.lifetime = True
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_domain"),
|
||||
data={"form-name": "create", "domain": get_email_domain_part(user.email)},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
b"You cannot add a domain that you are currently using for your personal email"
|
||||
in r.data
|
||||
)
|
||||
|
||||
|
||||
def test_add_domain_used_in_mailbox(flask_client):
|
||||
"""cannot add domain if it has been used in a verified mailbox"""
|
||||
user = login(flask_client)
|
||||
user.lifetime = True
|
||||
Session.commit()
|
||||
|
||||
Mailbox.create(
|
||||
user_id=user.id, email="mailbox@new-domain.com", verified=True, commit=True
|
||||
)
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.custom_domain"),
|
||||
data={"form-name": "create", "domain": "new-domain.com"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert b"new-domain.com already used in a SimpleLogin mailbox" in r.data
|
84
app/tests/dashboard/test_directory.py
Normal file
84
app/tests/dashboard/test_directory.py
Normal file
@ -0,0 +1,84 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.config import MAX_NB_DIRECTORY
|
||||
from app.models import Directory
|
||||
from tests.utils import login, random_token
|
||||
|
||||
|
||||
def test_create_directory(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
directory_name = random_token()
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.directory"),
|
||||
data={"form-name": "create", "name": directory_name},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert f"Directory {directory_name} is created" in r.data.decode()
|
||||
assert Directory.get_by(name=directory_name) is not None
|
||||
|
||||
|
||||
def test_delete_directory(flask_client):
|
||||
"""cannot add domain if user personal email uses this domain"""
|
||||
user = login(flask_client)
|
||||
directory_name = random_token()
|
||||
directory = Directory.create(name=directory_name, user_id=user.id, commit=True)
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.directory"),
|
||||
data={"form-name": "delete", "directory_id": directory.id},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert f"Directory {directory_name} has been deleted" in r.data.decode()
|
||||
assert Directory.get_by(name=directory_name) is None
|
||||
|
||||
|
||||
def test_create_directory_in_trash(flask_client):
|
||||
user = login(flask_client)
|
||||
directory_name = random_token()
|
||||
|
||||
directory = Directory.create(name=directory_name, user_id=user.id, commit=True)
|
||||
|
||||
# delete the directory
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.directory"),
|
||||
data={"form-name": "delete", "directory_id": directory.id},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert Directory.get_by(name=directory_name) is None
|
||||
|
||||
# try to recreate the directory
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.directory"),
|
||||
data={"form-name": "create", "name": directory_name},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
f"{directory_name} has been used before and cannot be reused" in r.data.decode()
|
||||
)
|
||||
|
||||
|
||||
def test_create_directory_out_of_quota(flask_client):
|
||||
user = login(flask_client)
|
||||
|
||||
for i in range(
|
||||
MAX_NB_DIRECTORY - Directory.filter(Directory.user_id == user.id).count()
|
||||
):
|
||||
Directory.create(name=f"test{i}", user_id=user.id, commit=True)
|
||||
|
||||
assert Directory.filter(Directory.user_id == user.id).count() == MAX_NB_DIRECTORY
|
||||
|
||||
flask_client.post(
|
||||
url_for("dashboard.directory"),
|
||||
data={"form-name": "create", "name": "test"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
# no new directory is created
|
||||
assert Directory.filter(Directory.user_id == user.id).count() == MAX_NB_DIRECTORY
|
41
app/tests/dashboard/test_index.py
Normal file
41
app/tests/dashboard/test_index.py
Normal file
@ -0,0 +1,41 @@
|
||||
from flask import url_for, g
|
||||
|
||||
from app import config
|
||||
from app.models import (
|
||||
Alias,
|
||||
)
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_create_random_alias_success(flask_client):
|
||||
user = login(flask_client)
|
||||
assert Alias.filter(Alias.user_id == user.id).count() == 1
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.index"),
|
||||
data={"form-name": "create-random-email"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert Alias.filter(Alias.user_id == user.id).count() == 2
|
||||
|
||||
|
||||
def test_too_many_requests(flask_client):
|
||||
config.DISABLE_RATE_LIMIT = False
|
||||
login(flask_client)
|
||||
|
||||
# can't create more than 5 aliases in 1 minute
|
||||
for _ in range(7):
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.index"),
|
||||
data={"form-name": "create-random-email"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
# to make flask-limiter work with unit test
|
||||
# https://github.com/alisaifee/flask-limiter/issues/147#issuecomment-642683820
|
||||
g._rate_limiting_complete = False
|
||||
else:
|
||||
# last request
|
||||
assert r.status_code == 429
|
||||
assert "Whoa, slow down there, pardner!" in str(r.data)
|
28
app/tests/dashboard/test_setting.py
Normal file
28
app/tests/dashboard/test_setting.py
Normal file
@ -0,0 +1,28 @@
|
||||
from flask import url_for
|
||||
|
||||
from app import config
|
||||
from app.models import EmailChange
|
||||
from app.utils import canonicalize_email
|
||||
from tests.utils import login, random_email, create_new_user
|
||||
|
||||
|
||||
def test_setup_done(flask_client):
|
||||
config.SKIP_MX_LOOKUP_ON_CHECK = True
|
||||
user = create_new_user()
|
||||
login(flask_client, user)
|
||||
noncanonical_email = f"nonca.{random_email()}"
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.setting"),
|
||||
data={
|
||||
"form-name": "update-email",
|
||||
"email": noncanonical_email,
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
email_change = EmailChange.get_by(user_id=user.id)
|
||||
assert email_change is not None
|
||||
assert email_change.new_email == canonicalize_email(noncanonical_email)
|
||||
config.SKIP_MX_LOOKUP_ON_CHECK = False
|
16
app/tests/dashboard/test_setup_done.py
Normal file
16
app/tests/dashboard/test_setup_done.py
Normal file
@ -0,0 +1,16 @@
|
||||
from flask import url_for
|
||||
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_setup_done(flask_client):
|
||||
login(flask_client)
|
||||
|
||||
r = flask_client.get(
|
||||
url_for("dashboard.setup_done"),
|
||||
)
|
||||
|
||||
assert r.status_code == 302
|
||||
# user is redirected to the dashboard page
|
||||
assert r.headers["Location"].endswith("/dashboard/")
|
||||
assert "setup_done=true" in r.headers["Set-Cookie"]
|
141
app/tests/dashboard/test_subdomain.py
Normal file
141
app/tests/dashboard/test_subdomain.py
Normal file
@ -0,0 +1,141 @@
|
||||
from flask import url_for
|
||||
|
||||
from app.config import MAX_NB_SUBDOMAIN
|
||||
from app.db import Session
|
||||
from app.models import SLDomain, CustomDomain, Job
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def setup_sl_domain() -> SLDomain:
|
||||
"""Take the first SLDomain and set its can_use_subdomain=True"""
|
||||
sl_domain: SLDomain = SLDomain.first()
|
||||
sl_domain.can_use_subdomain = True
|
||||
Session.commit()
|
||||
|
||||
return sl_domain
|
||||
|
||||
|
||||
def test_create_subdomain(flask_client):
|
||||
login(flask_client)
|
||||
sl_domain = setup_sl_domain()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.subdomain_route"),
|
||||
data={"form-name": "create", "subdomain": "test", "domain": sl_domain.domain},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert f"New subdomain test.{sl_domain.domain} is created" in r.data.decode()
|
||||
assert CustomDomain.get_by(domain=f"test.{sl_domain.domain}") is not None
|
||||
|
||||
|
||||
def test_delete_subdomain(flask_client):
|
||||
user = login(flask_client)
|
||||
sl_domain = setup_sl_domain()
|
||||
|
||||
subdomain = CustomDomain.create(
|
||||
domain=f"test.{sl_domain.domain}",
|
||||
user_id=user.id,
|
||||
is_sl_subdomain=True,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
nb_job = Job.count()
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.domain_detail", custom_domain_id=subdomain.id),
|
||||
data={"form-name": "delete"},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert f"test.{sl_domain.domain} scheduled for deletion." in r.data.decode()
|
||||
|
||||
# a domain deletion job is scheduled
|
||||
assert Job.count() == nb_job + 1
|
||||
|
||||
|
||||
def test_create_subdomain_in_trash(flask_client):
|
||||
user = login(flask_client)
|
||||
sl_domain = setup_sl_domain()
|
||||
|
||||
subdomain = CustomDomain.create(
|
||||
domain=f"test.{sl_domain.domain}",
|
||||
user_id=user.id,
|
||||
is_sl_subdomain=True,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# delete the subdomain
|
||||
CustomDomain.delete(subdomain.id)
|
||||
assert CustomDomain.get_by(domain=f"test.{sl_domain.domain}") is None
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("dashboard.subdomain_route"),
|
||||
data={"form-name": "create", "subdomain": "test", "domain": sl_domain.domain},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert (
|
||||
f"test.{sl_domain.domain} has been used before and cannot be reused"
|
||||
in r.data.decode()
|
||||
)
|
||||
|
||||
|
||||
def test_create_subdomain_out_of_quota(flask_client):
|
||||
user = login(flask_client)
|
||||
sl_domain = setup_sl_domain()
|
||||
|
||||
for i in range(MAX_NB_SUBDOMAIN):
|
||||
CustomDomain.create(
|
||||
domain=f"test{i}.{sl_domain.domain}",
|
||||
user_id=user.id,
|
||||
is_sl_subdomain=True,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert CustomDomain.filter_by(user_id=user.id).count() == MAX_NB_SUBDOMAIN
|
||||
|
||||
flask_client.post(
|
||||
url_for("dashboard.subdomain_route"),
|
||||
data={"form-name": "create", "subdomain": "test", "domain": sl_domain.domain},
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
# no new subdomain is created
|
||||
assert CustomDomain.filter_by(user_id=user.id).count() == MAX_NB_SUBDOMAIN
|
||||
|
||||
|
||||
def test_create_subdomain_invalid(flask_client):
|
||||
user = login(flask_client)
|
||||
sl_domain = setup_sl_domain()
|
||||
|
||||
# subdomain can't end with dash (-)
|
||||
flask_client.post(
|
||||
url_for("dashboard.subdomain_route"),
|
||||
data={"form-name": "create", "subdomain": "test-", "domain": sl_domain.domain},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert CustomDomain.filter_by(user_id=user.id).count() == 0
|
||||
|
||||
# subdomain can't contain underscore (_)
|
||||
flask_client.post(
|
||||
url_for("dashboard.subdomain_route"),
|
||||
data={
|
||||
"form-name": "create",
|
||||
"subdomain": "test_test",
|
||||
"domain": sl_domain.domain,
|
||||
},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert CustomDomain.filter_by(user_id=user.id).count() == 0
|
||||
|
||||
# subdomain must have at least 3 characters
|
||||
flask_client.post(
|
||||
url_for("dashboard.subdomain_route"),
|
||||
data={"form-name": "create", "subdomain": "te", "domain": sl_domain.domain},
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert CustomDomain.filter_by(user_id=user.id).count() == 0
|
35
app/tests/dashboard/test_unsubscribe.py
Normal file
35
app/tests/dashboard/test_unsubscribe.py
Normal file
@ -0,0 +1,35 @@
|
||||
from app.db import Session
|
||||
from app.models import (
|
||||
Alias,
|
||||
Contact,
|
||||
)
|
||||
from tests.utils import login
|
||||
|
||||
|
||||
def test_disable_alias(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
assert alias.enabled
|
||||
flask_client.post(f"/dashboard/unsubscribe/{alias.id}")
|
||||
assert not alias.enabled
|
||||
|
||||
|
||||
def test_block_contact(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.first()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="re1@SL",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert not contact.block_forward
|
||||
flask_client.post(f"/dashboard/block_contact/{contact.id}")
|
||||
assert contact.block_forward
|
||||
|
||||
# make sure the page loads
|
||||
flask_client.get(f"/dashboard/block_contact/{contact.id}")
|
BIN
app/tests/data/1px.jpg
Normal file
BIN
app/tests/data/1px.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 631 B |
BIN
app/tests/data/1px.webp
Normal file
BIN
app/tests/data/1px.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 44 B |
0
app/tests/email_tests/__init__.py
Normal file
0
app/tests/email_tests/__init__.py
Normal file
109
app/tests/email_tests/test_rate_limit.py
Normal file
109
app/tests/email_tests/test_rate_limit.py
Normal file
@ -0,0 +1,109 @@
|
||||
import random
|
||||
|
||||
from app.config import (
|
||||
MAX_ACTIVITY_DURING_MINUTE_PER_ALIAS,
|
||||
MAX_ACTIVITY_DURING_MINUTE_PER_MAILBOX,
|
||||
)
|
||||
from app.db import Session
|
||||
from app.email.rate_limit import (
|
||||
rate_limited_forward_phase,
|
||||
rate_limited_for_alias,
|
||||
rate_limited_for_mailbox,
|
||||
rate_limited_reply_phase,
|
||||
)
|
||||
from app.models import Alias, EmailLog, Contact
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def test_rate_limited_forward_phase_for_alias(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
# no rate limiting for a new alias
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
assert not rate_limited_for_alias(alias)
|
||||
|
||||
# rate limit when there's a previous activity on alias
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
)
|
||||
Session.commit()
|
||||
for _ in range(MAX_ACTIVITY_DURING_MINUTE_PER_ALIAS + 1):
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
assert rate_limited_for_alias(alias)
|
||||
|
||||
|
||||
def test_rate_limited_forward_phase_for_mailbox(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
)
|
||||
Session.commit()
|
||||
for _ in range(MAX_ACTIVITY_DURING_MINUTE_PER_MAILBOX + 1):
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
)
|
||||
|
||||
# Create another alias with the same mailbox
|
||||
# will be rate limited as there's a previous activity on mailbox
|
||||
alias2 = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
assert rate_limited_for_mailbox(alias2)
|
||||
|
||||
|
||||
def test_rate_limited_forward_phase(flask_client):
|
||||
# no rate limiting when alias does not exist
|
||||
assert not rate_limited_forward_phase("not-exist@alias.com")
|
||||
|
||||
|
||||
def test_rate_limited_reply_phase(flask_client):
|
||||
# no rate limiting when reply_email does not exist
|
||||
assert not rate_limited_reply_phase("not-exist-reply@alias.com")
|
||||
|
||||
user = create_new_user()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
reply_email = f"reply-{random.random()}@sl.local"
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email=reply_email,
|
||||
)
|
||||
Session.commit()
|
||||
for _ in range(MAX_ACTIVITY_DURING_MINUTE_PER_ALIAS + 1):
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
assert rate_limited_reply_phase(reply_email)
|
28
app/tests/example_emls/5xx_overwrite_spf.eml
Normal file
28
app/tests/example_emls/5xx_overwrite_spf.eml
Normal file
@ -0,0 +1,28 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: somewhere@rainbow.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_NA(0.10);
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
TO_DN_NONE(0.00)[];
|
||||
{{ spf_result }}(0.00[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
|
||||
This is a test mailing
|
27
app/tests/example_emls/dmarc_allow.eml
Normal file
27
app/tests/example_emls/dmarc_allow.eml
Normal file
@ -0,0 +1,27 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: spoofedemailsource@gmail.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_POLICY_ALLOW(0.10);
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
TO_DN_NONE(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
|
||||
This is a test mailing
|
27
app/tests/example_emls/dmarc_bad_policy.eml
Normal file
27
app/tests/example_emls/dmarc_bad_policy.eml
Normal file
@ -0,0 +1,27 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: spoofedemailsource@gmail.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_BAD_POLICY(0.10);
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
TO_DN_NONE(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
|
||||
This is a test mailing
|
41
app/tests/example_emls/dmarc_cannot_parse_rspamd_score.eml
Normal file
41
app/tests/example_emls/dmarc_cannot_parse_rspamd_score.eml
Normal file
@ -0,0 +1,41 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: spoofedemailsource@gmail.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [WRONGLY_FORMATTED / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_POLICY_SOFTFAIL(0.10)[gmail.com : No valid SPF, No valid DKIM,none];
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
FROM_EQ_ENVFROM(0.00)[];
|
||||
ASN(0.00)[asn:16276, ipnet:34.59.0.0/16, country:FR];
|
||||
R_DKIM_NA(0.00)[];
|
||||
RCVD_COUNT_ZERO(0.00)[0];
|
||||
FREEMAIL_ENVFROM(0.00)[gmail.com];
|
||||
FROM_NO_DN(0.00)[];
|
||||
R_SPF_SOFTFAIL(0.00)[~all];
|
||||
FORCE_ACTION_SL_SPF_FAIL_ADD_HEADER(0.00)[add header];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
FREEMAIL_FROM(0.00)[gmail.com];
|
||||
TO_DN_NONE(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
X-Rspamd-Pre-Result: action=add header;
|
||||
module=force_actions;
|
||||
unknown reason
|
||||
X-Spam: Yes
|
||||
|
||||
This is a test mailing
|
41
app/tests/example_emls/dmarc_gmail_softfail.eml
Normal file
41
app/tests/example_emls/dmarc_gmail_softfail.eml
Normal file
@ -0,0 +1,41 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: spoofedemailsource@gmail.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_POLICY_SOFTFAIL(0.10)[gmail.com : No valid SPF, No valid DKIM,none];
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
FROM_EQ_ENVFROM(0.00)[];
|
||||
ASN(0.00)[asn:16276, ipnet:34.59.0.0/16, country:FR];
|
||||
R_DKIM_NA(0.00)[];
|
||||
RCVD_COUNT_ZERO(0.00)[0];
|
||||
FREEMAIL_ENVFROM(0.00)[gmail.com];
|
||||
FROM_NO_DN(0.00)[];
|
||||
R_SPF_SOFTFAIL(0.00)[~all];
|
||||
FORCE_ACTION_SL_SPF_FAIL_ADD_HEADER(0.00)[add header];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
FREEMAIL_FROM(0.00)[gmail.com];
|
||||
TO_DN_NONE(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
X-Rspamd-Pre-Result: action=add header;
|
||||
module=force_actions;
|
||||
unknown reason
|
||||
X-Spam: Yes
|
||||
|
||||
This is a test mailing
|
27
app/tests/example_emls/dmarc_na.eml
Normal file
27
app/tests/example_emls/dmarc_na.eml
Normal file
@ -0,0 +1,27 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: spoofedemailsource@gmail.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_NA(0.10);
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
TO_DN_NONE(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
|
||||
This is a test mailing
|
41
app/tests/example_emls/dmarc_quarantine.eml
Normal file
41
app/tests/example_emls/dmarc_quarantine.eml
Normal file
@ -0,0 +1,41 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: spoofedemailsource@gmail.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_POLICY_QUARANTINE(0.10)[gmail.com : No valid SPF, No valid DKIM,none];
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
FROM_EQ_ENVFROM(0.00)[];
|
||||
ASN(0.00)[asn:16276, ipnet:34.59.0.0/16, country:FR];
|
||||
R_DKIM_NA(0.00)[];
|
||||
RCVD_COUNT_ZERO(0.00)[0];
|
||||
FREEMAIL_ENVFROM(0.00)[gmail.com];
|
||||
FROM_NO_DN(0.00)[];
|
||||
R_SPF_SOFTFAIL(0.00)[~all];
|
||||
FORCE_ACTION_SL_SPF_FAIL_ADD_HEADER(0.00)[add header];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
FREEMAIL_FROM(0.00)[gmail.com];
|
||||
TO_DN_NONE(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
X-Rspamd-Pre-Result: action=add header;
|
||||
module=force_actions;
|
||||
unknown reason
|
||||
X-Spam: Yes
|
||||
|
||||
This is a test mailing
|
41
app/tests/example_emls/dmarc_reject.eml
Normal file
41
app/tests/example_emls/dmarc_reject.eml
Normal file
@ -0,0 +1,41 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: spoofedemailsource@gmail.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_POLICY_REJECT(0.10)[gmail.com : No valid SPF, No valid DKIM,none];
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
FROM_EQ_ENVFROM(0.00)[];
|
||||
ASN(0.00)[asn:16276, ipnet:34.59.0.0/16, country:FR];
|
||||
R_DKIM_NA(0.00)[];
|
||||
RCVD_COUNT_ZERO(0.00)[0];
|
||||
FREEMAIL_ENVFROM(0.00)[gmail.com];
|
||||
FROM_NO_DN(0.00)[];
|
||||
R_SPF_SOFTFAIL(0.00)[~all];
|
||||
FORCE_ACTION_SL_SPF_FAIL_ADD_HEADER(0.00)[add header];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
FREEMAIL_FROM(0.00)[gmail.com];
|
||||
TO_DN_NONE(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
X-Rspamd-Pre-Result: action=add header;
|
||||
module=force_actions;
|
||||
unknown reason
|
||||
X-Spam: Yes
|
||||
|
||||
This is a test mailing
|
25
app/tests/example_emls/dmarc_reply_check.eml
Normal file
25
app/tests/example_emls/dmarc_reply_check.eml
Normal file
@ -0,0 +1,25 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ contact_email }}
|
||||
From: {{ alias_email }}
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
{{ dmarc_result }}(0.00)[];
|
||||
X-Rspamd-Pre-Result: action=add header;
|
||||
module=force_actions;
|
||||
unknown reason
|
||||
X-Spam: Yes
|
||||
|
||||
This is a test mailing
|
42
app/tests/example_emls/double_queue_id_header.eml
Normal file
42
app/tests/example_emls/double_queue_id_header.eml
Normal file
@ -0,0 +1,42 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: wehrman_mannequin@sldev.ovh
|
||||
From: spoofedemailsource@gmail.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: INVALIDVALUE
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
X-Spamd-Result: default: False [0.50 / 13.00];
|
||||
MID_RHS_NOT_FQDN(0.50)[];
|
||||
DMARC_POLICY_SOFTFAIL(0.10)[gmail.com : No valid SPF, No valid DKIM,none];
|
||||
MIME_GOOD(-0.10)[text/plain];
|
||||
MIME_TRACE(0.00)[0:+];
|
||||
FROM_EQ_ENVFROM(0.00)[];
|
||||
ASN(0.00)[asn:16276, ipnet:34.59.0.0/16, country:FR];
|
||||
R_DKIM_NA(0.00)[];
|
||||
RCVD_COUNT_ZERO(0.00)[0];
|
||||
FREEMAIL_ENVFROM(0.00)[gmail.com];
|
||||
FROM_NO_DN(0.00)[];
|
||||
R_SPF_SOFTFAIL(0.00)[~all];
|
||||
FORCE_ACTION_SL_SPF_FAIL_ADD_HEADER(0.00)[add header];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
FREEMAIL_FROM(0.00)[gmail.com];
|
||||
TO_DN_NONE(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
ARC_NA(0.00)[]
|
||||
X-Rspamd-Pre-Result: action=add header;
|
||||
module=force_actions;
|
||||
unknown reason
|
||||
X-Spam: Yes
|
||||
|
||||
This is a test mailing
|
258
app/tests/example_emls/hotmail_complaint.eml
Normal file
258
app/tests/example_emls/hotmail_complaint.eml
Normal file
@ -0,0 +1,258 @@
|
||||
X-SimpleLogin-Client-IP: 40.92.66.13
|
||||
Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=40.92.66.13;
|
||||
helo=eur01-ve1-obe.outbound.protection.outlook.com;
|
||||
envelope-from=staff@hotmail.com; receiver=<UNKNOWN>
|
||||
Received: from EUR01-VE1-obe.outbound.protection.outlook.com
|
||||
(mail-oln040092066013.outbound.protection.outlook.com [40.92.66.13])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by prod4.simplelogin.co (Postfix) with ESMTPS id 408E09C472
|
||||
for <{{ postmaster }}>; Mon, 9 May 2022 13:11:34 +0000 (UTC)
|
||||
ARC-Seal: i=1; a=rsa-sha256; s=arcselector9901; d=microsoft.com; cv=none;
|
||||
b=V3N8KdYGgYrjs5KcjFUA0MgPUmOc+NV4ygLfSd7fehfiNemKdhe6Cpfj58zWFNzoG5qBoUCIm/BI7aCr7lqAU2hQJypTrJG+3zbSdnuCKMBVV5GHZxkE+XAeSU+4wt4xwl1ZiVx/2P//xUVWN/TVmiuKUgCn9n+WagU9LYGVT9z6wwOpXggpDf6ow9RnJDPJpkakHRh7rQPABbrOpVqEZnoJdAH5mgdTHJOeBumNym4i3GKnky+IfMlqwGcbTrzgrt/D3PpZdsMG4B+jEHtTo3FgB9JY+abjU9Bvn4rXwKr3RMF+1ZV3UsznQVwuT99PtfEcExV3zSsqEPDBy9QT9w==
|
||||
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com;
|
||||
s=arcselector9901;
|
||||
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1;
|
||||
bh=Y37p6EaXY5hpBNgMr1ILYzy35GKdkqWXm69FR2RyQgA=;
|
||||
b=aet1P4fpmUM9bqbLD3vtp/EWfUi2WfvWbOnnLg/YZ2vxoTF/eM5IHDBB/I7btdzZICric+KkhRih/kvaVURGy4jybYjn9FNfT+HShTJa75Pk30fp3in/5lL2x6Q0xM0Naf9YtTvGgqlLDrdgCmktxyByNAOFPo27fEWy3fk/00IPWyI8j77VvYsGn8rJCLbhDUBWwGzQ9P7SabIqn9Ybx6CKcw2FssJhSNAyOIx7EkrGxq8y/5dXeWSHLFBdHPu6F9w/DKyt9cv17rBSnHo4tx1Ese93vBHT5XIwTwnGisCa0++eqL/69GugKoe5odkAfsdRAlBjVTgXp2Lol4rrpg==
|
||||
ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=none; dmarc=none;
|
||||
dkim=none; arc=none
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=hotmail.com;
|
||||
s=selector1;
|
||||
h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck;
|
||||
bh=Y37p6EaXY5hpBNgMr1ILYzy35GKdkqWXm69FR2RyQgA=;
|
||||
b=uMkd90Lx6ikNpk7RRBU3AfQ0jjbjRZAGQLnY3r+dQ3CNnhgfHxpNRudxGDydmf6GQ2AuylmOnLVATh8XMKTvCnVg8hjB9xrxd5qPpQ3k92U5VlgVe1o1Nwq8R6VCJugOZduDjSJdBXO2ACosUul6IQXKMBpSNq+bGJ9VHu63EGTphkWOOw1a4PArg8tQTSmkpkyh788nsfNXnVsh2fkL6we1LyvagQzTS4e1ynuSk1zAk+6U5KOuhRVr2Nh/AvyvswWpjA4pflOqFwyqsMYb3N6wnpRTct8CJUPlQwEx6chiJgKNGrAkdRbnWaEyeIEdyJB/NLwtPqZzKYFgv7f8wg==
|
||||
Received: from AM6PR02CA0021.eurprd02.prod.outlook.com (2603:10a6:20b:6e::34)
|
||||
by AM0PR02MB4563.eurprd02.prod.outlook.com (2603:10a6:208:ec::33) with
|
||||
Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5227.22; Mon, 9 May
|
||||
2022 13:11:32 +0000
|
||||
Received: from AM6EUR05FT047.eop-eur05.prod.protection.outlook.com
|
||||
(2603:10a6:20b:6e:cafe::26) by AM6PR02CA0021.outlook.office365.com
|
||||
(2603:10a6:20b:6e::34) with Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5227.23 via Frontend
|
||||
Transport; Mon, 9 May 2022 13:11:32 +0000
|
||||
Received: from DM5SVC01SF077 (40.107.211.126) by
|
||||
AM6EUR05FT047.mail.protection.outlook.com (10.233.241.167) with Microsoft
|
||||
SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384) id
|
||||
15.20.5227.15 via Frontend Transport; Mon, 9 May 2022 13:11:32 +0000
|
||||
X-IncomingTopHeaderMarker:
|
||||
OriginalChecksum:86053024C4DD515561A96BAF61AACB6F8A4DB30C8D14CAC5F2F7D189ACDCA109;UpperCasedChecksum:5323AB267D58619B82076460438A30DFDD8E7969870D76B723156F921928319B;SizeAsReceived:257;Count:6
|
||||
Date: Mon, 9 May 2022 13:10:08 +0000
|
||||
From: <staff@hotmail.com>
|
||||
Subject: complaint about message from 176.119.200.162
|
||||
To: {{ postmaster }}
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="31A9507D-D0B3-4DCD-AFBB-413468892CFE"
|
||||
X-IncomingHeaderCount: 6
|
||||
Message-ID:
|
||||
<1d63d9ee-8f3e-4876-955c-1807db5ad138@AM6EUR05FT047.eop-eur05.prod.protection.outlook.com>
|
||||
X-EOPAttributedMessage: 0
|
||||
X-MS-PublicTrafficType: Email
|
||||
X-MS-Office365-Filtering-Correlation-Id: 44e9ec0b-6c5d-4cea-6417-08da31bd7000
|
||||
X-MS-TrafficTypeDiagnostic: AM0PR02MB4563:EE_
|
||||
X-Microsoft-Antispam: BCL:0;
|
||||
X-Microsoft-Antispam-Message-Info:
|
||||
lK5xD4UZS47NfR0tHc3wEp4HHOifZ4SDBb8aKx7H/vEW8Rg8rXXH12G4lWdpzr8qTsCmvzuhj5x6IAumOKQ8lWLj5Lp3jyml91wVnwCtUnk5cTXpQwDZd9QMgtEW07GoLdWjkbShAhLRDf+9Y4DxidHCacOAYxcNX42wo3vYZOEHDzVRUxSmY0c7Km60pDtiYzEk+P9AoE2YKYG2rDwDx0vgoLgqFspGqQ+2OeHD2ZAEyATHR/sQy6tf5S2d4wA3HcHrwrGMlz/4d9VbT5h9a5cqj9S59wpuc6g8nyYhmK3AHJkB5nXmpBZBihTw5X/Qh5PZqUYwPxkwpq3WlaEuXvzaKFiwJFvtuRGX+mEioClCxiwPROb7sI9ZHWPw48AHysF+whYGBfleRy4c2SuW6e1D5uewGry+lXVljxg7qKo=
|
||||
X-OriginatorOrg: sct-15-20-4755-11-msonline-outlook-ab7de.templateTenant
|
||||
X-MS-Exchange-CrossTenant-OriginalArrivalTime: 09 May 2022 13:11:32.0875
|
||||
(UTC)
|
||||
X-MS-Exchange-CrossTenant-Network-Message-Id:
|
||||
44e9ec0b-6c5d-4cea-6417-08da31bd7000
|
||||
X-MS-Exchange-CrossTenant-Id: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa
|
||||
X-MS-Exchange-CrossTenant-AuthSource:
|
||||
AM6EUR05FT047.eop-eur05.prod.protection.outlook.com
|
||||
X-MS-Exchange-CrossTenant-AuthAs: Anonymous
|
||||
X-MS-Exchange-CrossTenant-FromEntityHeader: Internet
|
||||
X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg:
|
||||
00000000-0000-0000-0000-000000000000
|
||||
X-MS-Exchange-Transport-CrossTenantHeadersStamped: AM0PR02MB4563
|
||||
X-Spamd-Result: default: False [-1.75 / 13.00];
|
||||
ARC_ALLOW(-1.00)[microsoft.com:s=arcselector9901:i=1];
|
||||
DMARC_POLICY_ALLOW(-0.50)[hotmail.com,none];
|
||||
R_SPF_ALLOW(-0.20)[+ip4:40.92.0.0/15];
|
||||
MIME_HTML_ONLY(0.20)[];
|
||||
R_DKIM_ALLOW(-0.20)[hotmail.com:s=selector1];
|
||||
MIME_GOOD(-0.10)[multipart/mixed,multipart/related];
|
||||
MANY_INVISIBLE_PARTS(0.05)[1];
|
||||
NEURAL_HAM(-0.00)[-0.996];
|
||||
FROM_EQ_ENVFROM(0.00)[];
|
||||
FREEMAIL_ENVFROM(0.00)[hotmail.com];
|
||||
MIME_TRACE(0.00)[0:+,1:~,2:+,3:+,4:~];
|
||||
ASN(0.00)[asn:8075, ipnet:40.80.0.0/12, country:US];
|
||||
RCVD_IN_DNSWL_NONE(0.00)[40.92.66.13:from];
|
||||
DKIM_TRACE(0.00)[hotmail.com:+];
|
||||
RCVD_TLS_LAST(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
FREEMAIL_FROM(0.00)[hotmail.com];
|
||||
FROM_NO_DN(0.00)[];
|
||||
TO_DN_NONE(0.00)[];
|
||||
RCVD_COUNT_THREE(0.00)[4];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
DWL_DNSWL_NONE(0.00)[hotmail.com:dkim]
|
||||
X-Rspamd-Queue-Id: 408E09C472
|
||||
X-Rspamd-Server: prod4
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
--31A9507D-D0B3-4DCD-AFBB-413468892CFE
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline
|
||||
|
||||
X-HmXmrOriginalRecipient: <jan.bailey2934@outlook.com>
|
||||
X-MS-Exchange-EOPDirect: true
|
||||
Received: from SJ0PR11MB4958.namprd11.prod.outlook.com (2603:10b6:a03:2ae::24)
|
||||
by SA0PR11MB4525.namprd11.prod.outlook.com with HTTPS; Mon, 9 May 2022
|
||||
04:30:48 +0000
|
||||
Received: from BN9PR03CA0117.namprd03.prod.outlook.com (2603:10b6:408:fd::32)
|
||||
by SJ0PR11MB4958.namprd11.prod.outlook.com (2603:10b6:a03:2ae::24) with
|
||||
Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5227.20; Mon, 9 May
|
||||
2022 04:30:45 +0000
|
||||
Received: from BN8NAM11FT053.eop-nam11.prod.protection.outlook.com
|
||||
(2603:10b6:408:fd:cafe::d0) by BN9PR03CA0117.outlook.office365.com
|
||||
(2603:10b6:408:fd::32) with Microsoft SMTP Server (version=TLS1_2,
|
||||
cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.20.5227.20 via Frontend
|
||||
Transport; Mon, 9 May 2022 04:30:45 +0000
|
||||
Authentication-Results: spf=pass (sender IP is 176.119.200.162)
|
||||
smtp.mailfrom=simplelogin.co; dkim=pass (signature was verified)
|
||||
header.d=simplelogin.co;dmarc=pass action=none
|
||||
header.from=simplelogin.co;compauth=pass reason=100
|
||||
Received-SPF: Pass (protection.outlook.com: domain of simplelogin.co
|
||||
designates 176.119.200.162 as permitted sender)
|
||||
receiver=protection.outlook.com; client-ip=176.119.200.162;
|
||||
helo=mail-200162.simplelogin.co;
|
||||
Received: from mail-200162.simplelogin.co (176.119.200.162) by
|
||||
BN8NAM11FT053.mail.protection.outlook.com (10.13.177.209) with Microsoft SMTP
|
||||
Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id
|
||||
15.20.5227.15 via Frontend Transport; Mon, 9 May 2022 04:30:44 +0000
|
||||
X-IncomingTopHeaderMarker:
|
||||
OriginalChecksum:5EBD8C309CA888838EDC898C63E28E1EC00EF74772276A54C08DA83D658756F4;UpperCasedChecksum:E102374CD208D4ACB2034F1A17F76DA6345BD176395C6D4EADEC3B47BFF41ECC;SizeAsReceived:1262;Count:15
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=simplelogin.co;
|
||||
s=dkim; t=1652070640; h=From:To:Subject:Message-ID:Date;
|
||||
bh=Tu2Q0oO5GuGw4CVxDAdixtRKr6jqMWjpH9zEf50uKwg=;
|
||||
b=o6I0Ij1CahU9EUj/9uwWJpsDjfi/2gQIXT0KJT6IAK9hOoJ5bVqPsqtyGTfIoqYhhtD/ic
|
||||
5NybKJmB6B6KL5hl5LG3KzCdaWfe3dAAhD4e2gIU80dal596dlzluyvLR1k+6rdM4JvlGq
|
||||
OVWLR42Oj4anrnOqLCUkL44ILIhLpAE=
|
||||
Date: Mon, 9 May 2022 00:30:38 -0400 (EDT)
|
||||
Message-ID:
|
||||
<10627474.1041327707.1652070638478.JavaMail.cloud@p2-mta-0301.p2.messagegears.net>
|
||||
Subject: Original Subject
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="----=_Part_1041327705_575167926.1652070638478"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
X-SimpleLogin-Type: Forward
|
||||
X-SimpleLogin-EmailLog-ID: 832832
|
||||
X-SimpleLogin-Envelope-To: {{ rcpt }}
|
||||
From: {{ sender }}
|
||||
Reply-To: {{ sender }}
|
||||
To: {{ rcpt_comma_list }}
|
||||
List-Unsubscribe: <mailto:unsubscribe@simplelogin.co?subject=3134388=>
|
||||
X-SimpleLogin-Want-Signing: yes
|
||||
X-IncomingHeaderCount: 15
|
||||
Return-Path: {{ return_path }}
|
||||
X-MS-Exchange-Organization-ExpirationStartTime: 09 May 2022 04:30:45.1195
|
||||
(UTC)
|
||||
X-MS-Exchange-Organization-ExpirationStartTimeReason: OriginalSubmit
|
||||
X-MS-Exchange-Organization-ExpirationInterval: 1:00:00:00.0000000
|
||||
X-MS-Exchange-Organization-ExpirationIntervalReason: OriginalSubmit
|
||||
X-MS-Exchange-Organization-Network-Message-Id:
|
||||
ede92e41-5acb-4474-c5be-08da3174af2b
|
||||
X-EOPAttributedMessage: 0
|
||||
X-EOPTenantAttributedMessage: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa:0
|
||||
X-MS-Exchange-Organization-MessageDirectionality: Incoming
|
||||
X-MS-PublicTrafficType: Email
|
||||
X-MS-Exchange-Organization-AuthSource:
|
||||
BN8NAM11FT053.eop-nam11.prod.protection.outlook.com
|
||||
X-MS-Exchange-Organization-AuthAs: Anonymous
|
||||
X-MS-UserLastLogonTime: 5/9/2022 3:30:52 AM
|
||||
X-MS-Office365-Filtering-Correlation-Id: ede92e41-5acb-4474-c5be-08da3174af2b
|
||||
X-MS-TrafficTypeDiagnostic: SJ0PR11MB4958:EE_
|
||||
X-MS-Exchange-EOPDirect: true
|
||||
X-Sender-IP: 176.119.200.162
|
||||
X-SID-PRA: PHWNQHFTTLQNZJXKMLHZCSKLLLJXMGEJOEOWW@SIMPLELOGIN.CO
|
||||
X-SID-Result: PASS
|
||||
X-MS-Exchange-Organization-PCL: 2
|
||||
X-MS-Exchange-Organization-SCL: 1
|
||||
X-Microsoft-Antispam: BCL:0;
|
||||
X-MS-Exchange-CrossTenant-OriginalArrivalTime: 09 May 2022 04:30:44.9945
|
||||
(UTC)
|
||||
X-MS-Exchange-CrossTenant-Network-Message-Id:
|
||||
ede92e41-5acb-4474-c5be-08da3174af2b
|
||||
X-MS-Exchange-CrossTenant-Id: 84df9e7f-e9f6-40af-b435-aaaaaaaaaaaa
|
||||
X-MS-Exchange-CrossTenant-AuthSource:
|
||||
BN8NAM11FT053.eop-nam11.prod.protection.outlook.com
|
||||
X-MS-Exchange-CrossTenant-AuthAs: Anonymous
|
||||
X-MS-Exchange-CrossTenant-FromEntityHeader: Internet
|
||||
X-MS-Exchange-CrossTenant-RMS-PersistedConsumerOrg:
|
||||
00000000-0000-0000-0000-000000000000
|
||||
X-MS-Exchange-Transport-CrossTenantHeadersStamped: SJ0PR11MB4958
|
||||
X-MS-Exchange-Transport-EndToEndLatency: 00:00:03.3271765
|
||||
X-MS-Exchange-Processed-By-BccFoldering: 15.20.5227.023
|
||||
X-Microsoft-Antispam-Mailbox-Delivery:
|
||||
abwl:0;wl:0;pcwl:0;kl:0;iwl:0;ijl:0;dwl:0;dkl:0;rwl:0;ucf:0;jmr:0;ex:0;auth:1;dest:I;ENG:(5062000285)(90000117)(90005022)(91005020)(91035115)(5061607266)(5061608174)(9050020)(9100338)(2008001134)(2008000189)(2008120399)(2008019284)(2008021020)(8390246)(8377080)(8386120)(4810004)(4910013)(9910022)(9510006)(10110021)(9320005);
|
||||
X-Message-Info:
|
||||
5vMbyqxGkdcvoPRAk5ACFywqndfpuBMcVz6K/12RtMALmdfGi+GpgO+lXQe3PiGwHtV5wXFRStQwg29XySZZo6tOyvshTSJ1uafhX53S93r5MaqDxJrR0UNGr2VYdKiAm1jYIYQm84v/mEbSAGjjBwEgS1PHlzM72I96JadXzfV9Fmsd5pHlfoLxEqXe6hBJAAQS99CcpwPDnaVA9UZUHA==
|
||||
X-Message-Delivery: Vj0xLjE7dXM9MDtsPTA7YT0wO0Q9MTtHRD0xO1NDTD0tMQ==
|
||||
X-Microsoft-Antispam-Message-Info:
|
||||
=?utf-8?B?VjZIQkpKR05oRUo1Vzc0YTBDUW52S0lsYkJSMGRzY0hJMnRMOWdyRGowcGpk?=
|
||||
=?utf-8?B?SUJLSDRPaStzakpJUHlaWVFnNWpBSGRsZ1Z4aEFmaXJOR1ZMUWxTTnQ1SXg1?=
|
||||
=?utf-8?B?anhFNTJ5RGU2YjRiTWhWK3FvWXBJU29YSWdqM3VvUkZpY21aaW5lSkJ5WWph?=
|
||||
=?utf-8?B?L2pxclptbVBGdm02emlHT3ZBQ1BHZTcrM0c3NmJ5alJLSGlaYVMvK0hwVmJV?=
|
||||
=?utf-8?B?eHlTU2grSElBTVY5cXF2d250OXBmQ2pzeEVUWTlSZ1hCc1dEdStXMzFGcWlO?=
|
||||
=?utf-8?B?VytUeEgyRWl5a2U1Y09VKyt3am9ZQVYrRm1LUkhRRGdKbkFTaHc4RTErQ1c0?=
|
||||
=?utf-8?B?RjBNVllEVW9UakJIQm5FWWVYd2RuaENZTVJIUkI4RmlheWsyajZmanFCUlpt?=
|
||||
=?utf-8?B?ZTJYZlg1RGxkbEVlRk0zallRWStiU1Z1QmJlTmtKS3J5MmZuOFk2blRHemEw?=
|
||||
=?utf-8?B?OVhkUUhWWTAzV2dySnMra1pKMGo1Zy8xSFNuemx4Slg1ckhDcitmVGRHSDBW?=
|
||||
=?utf-8?B?MFlOMDFtNmRPTDVSL3BGU0VNNWRObGVkUUlRcG9MSUJFeVBFcGtlVENSZmIr?=
|
||||
=?utf-8?B?V3F6by8vOHBROWplTi9JdWtEVDFwUVZsdVk5djBtN0wzbk04RG56RjRsM1ZH?=
|
||||
=?utf-8?B?cytsajBZNUNwUXk5SVRFZXhMejN3anYweGpCWkltQ2lwQnA3V1B6UUt0VUw1?=
|
||||
=?utf-8?B?dXpLQ3hxemNQNWRGWmpqZi9BY2EzOTAwQ3h5RlF2RHQyVG1McWp6N1JXUWRY?=
|
||||
=?utf-8?B?TjlCRWFmNFhQSitwSTk2cEhPK1N3ZVQxbktlMWFwa05hNGllOVpCc2Q3MUEy?=
|
||||
=?utf-8?B?TlBHVE9YUE8xRUk3dndyNkFQVlhhN3JIMnUxL25pZ3JaM1hFS0VUOXNqT2NF?=
|
||||
=?utf-8?B?Y3lFcUM0dDVuOGhTdmJ1RjJJK2sxZGViOUU2SE1DTUZ1c0pSSlNsazdPWHJ5?=
|
||||
=?utf-8?B?TXo0dUUrZEhqaVpGTHNTUnNUTUl2L2hZeFhoNUVtcmJPQ0lXYnV5Yy8rSXBq?=
|
||||
=?utf-8?B?bjYwVlBET0ErZkQ4KzJsQmM5b0hUTXJSSWlhdXlNeTZ2a0xlaHp5ZTZRQnox?=
|
||||
=?utf-8?B?T2h2NkZKNmpLcDg4TCs5ckdoU3d5aEc1Q1FYUFdTOXhxcFJsaTdtZkVuNG1W?=
|
||||
=?utf-8?B?SkVsN2llT3FpTnB6Q3lMbDR4ZzVzblhLVWw3VkpJblRQQVA4cDd1aGdtbll4?=
|
||||
=?utf-8?B?U2RWQXplZjRreWhJRnQwWGhWT2pnVmxwTW9hdUxwRE9VaTJqd1lqenh3T2pK?=
|
||||
=?utf-8?B?R2ZMaDJmNm1lS25TNU56ODFBcnc1TUZQbi9pZ0hnampKNUl0MzVQRG5wenZH?=
|
||||
=?utf-8?B?dTdrcTA4VXUwZmdNaXBKMnVsY1phOEtLUEZWMzNnUlVxYXhrRDFUN3FFN0lZ?=
|
||||
=?utf-8?B?MnVzbmhVQ2kvQVkzZ3NBQnNGL0NCNlZTbmV5ZW9FVWg5dUJTbmtaQnNZemRT?=
|
||||
=?utf-8?B?cDFKUnRPU2VpNnNwM3V5eXJxMy9YbFhPYTRFSkEyTUZjSVlNaFV0UE5RbjhK?=
|
||||
=?utf-8?B?NjJmckpva2xuaGhYT2Jkb2g1U1NEaFJmQWc5bVhheGZYMXY1b2toaVRPOXNT?=
|
||||
=?utf-8?B?Y2ZhVjYyY0pnbmw4N3VneVR6bXFoRTlndE9lTzlac0JTRWFKc1BMTmNrNFMx?=
|
||||
=?utf-8?B?M0lwTXI3STZXcFNmbytNcFB2VzJFSFpLSWFpbjlzcVlVRHk3RTFIUUQzOUlB?=
|
||||
=?utf-8?B?YnR1eC9jUnVNWlhadktVKzM5MmdmR1pBTXVxK2xzUXZ4MzNUWW5rQXZ4SXMv?=
|
||||
=?utf-8?B?RnBLUmcwT3FUWENucWtuTWhBQnl5VWFpczNGUnBkQ0ltM2ttMDM1RnFScXFa?=
|
||||
=?utf-8?B?dEtNNnF4Q1FDS2RqRTRuRkNRUC9JVTdZZ216c3hycC9ZalptbDZNZ25ydWFp?=
|
||||
=?utf-8?B?Z25qMGFLK1FQYm0vUU40OSt1SVJBTmdPTVNRN2JTVmxLTlRJMkZDeldKYWNx?=
|
||||
=?utf-8?B?VEJEVHE5ZE9QNWsxZkxrb0pFOEU5cUJvT3ArOUFDMXlZM2N4Smk5ay9qQXEv?=
|
||||
=?utf-8?B?ZXc3ZjVHMjdkcjBkN1Rodmdyd1JldkFBeDlVblRVbkxrY0xhZkIwVzBpTlNM?=
|
||||
=?utf-8?B?THAvZ01hS3NVK0dHblFFQ0h6VXYydW1QaUwzM29zcjRYRFJRTU9NZWYxQ2Nw?=
|
||||
=?utf-8?B?N1liQ3g2ZUtveTdTaW1ZSGovLzNWbWh2bDd6ZXRUR3B3eEYwakVCOS95aEs0?=
|
||||
=?utf-8?B?NkkzL1dQREVlVHFXWmE4RktDUHFENVQwYW9YWE9LS2hrMzAyVWFXTDZFVkx5?=
|
||||
=?utf-8?B?cU1nZDkzOTR1dk40SHFIcHRDSVRPajMvSVAyd0JQNDJnaVoxNmhNOFEzdzlj?=
|
||||
=?utf-8?B?ODdUNXRIVkQvTHYzMytWY2o3UHZkdUNTR1pvSVJvclVCN01EZW5pVXdRUDgx?=
|
||||
=?utf-8?B?Vmg2aUdlOUJzdXlPdXFlL01raHZSbkRONncyRlFLcGpLUFR4bm9BQXVJMHJC?=
|
||||
=?utf-8?B?cWdJSFJwZEVkZjZkOTJqZG1FNHdZRWpGdUR6R2hjdHRoMTg1Z2lpeGpnZzlH?=
|
||||
=?utf-8?B?Um5WOEJINFBFM3Evdmt4VVRCQnAwd2xBRGVralpwRnV0eUhJNTluQzFLQXI2?=
|
||||
=?utf-8?B?NXI4amV3c0ZRZEZLRjE1ZEQ3aW90Y1I0K3NPN3ZoVyt1UVdzWUpQUGh1b25N?=
|
||||
=?utf-8?Q?amuRKzTLQzIrlx9Vmv+SjIosxogY=3D?=
|
||||
MIME-Version: 1.0
|
||||
|
||||
------=_Part_1041327705_575167926.1652070638478
|
||||
Content-Type: multipart/related;
|
||||
boundary="----=_Part_1041327706_445426653.1652070638478"
|
||||
|
||||
------=_Part_1041327706_445426653.1652070638478
|
||||
Content-Type: text/html;charset=UTF-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Here goes the original email content
|
||||
|
||||
------=_Part_1041327706_445426653.1652070638478--
|
||||
|
||||
------=_Part_1041327705_575167926.1652070638478--
|
||||
|
||||
--31A9507D-D0B3-4DCD-AFBB-413468892CFE--
|
25
app/tests/example_emls/multipart_alternative.eml
Normal file
25
app/tests/example_emls/multipart_alternative.eml
Normal file
@ -0,0 +1,25 @@
|
||||
Content-Type: multipart/alternative; boundary="===============5006593052976639648=="
|
||||
MIME-Version: 1.0
|
||||
Subject: My subject
|
||||
From: foo@example.org
|
||||
To: bar@example.net
|
||||
|
||||
--===============5006593052976639648==
|
||||
Content-Type: text/plain; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
This is HTML
|
||||
--===============5006593052976639648==
|
||||
Content-Type: text/html; charset="us-ascii"
|
||||
MIME-Version: 1.0
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<html>
|
||||
<body>
|
||||
This is <i>HTML</i>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
--===============5006593052976639648==--
|
||||
|
19
app/tests/example_emls/no_spamd_header.eml
Normal file
19
app/tests/example_emls/no_spamd_header.eml
Normal file
@ -0,0 +1,19 @@
|
||||
X-SimpleLogin-Client-IP: 54.39.200.130
|
||||
Received-SPF: Softfail (mailfrom) identity=mailfrom; client-ip=34.59.200.130;
|
||||
helo=relay.somewhere.net; envelope-from=everwaste@gmail.com;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from relay.somewhere.net (relay.somewhere.net [34.59.200.130])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.sldev.ovh (Postfix) with ESMTPS id 6D8C13F069
|
||||
for <wehrman_mannequin@sldev.ovh>; Thu, 17 Mar 2022 16:50:20 +0000 (UTC)
|
||||
Date: Thu, 17 Mar 2022 16:50:18 +0000
|
||||
To: {{ alias_email }}
|
||||
From: somewhere@rainbow.com
|
||||
Subject: test Thu, 17 Mar 2022 16:50:18 +0000
|
||||
Message-Id: <20220317165018.000191@somewhere-5488dd4b6b-7crp6>
|
||||
X-Mailer: swaks v20201014.0 jetmore.org/john/code/swaks/
|
||||
X-Rspamd-Queue-Id: 6D8C13F069
|
||||
X-Rspamd-Server: staging1
|
||||
|
||||
This is a test mailing
|
63
app/tests/example_emls/reference_encoded.eml
Normal file
63
app/tests/example_emls/reference_encoded.eml
Normal file
@ -0,0 +1,63 @@
|
||||
Received: by mail-ed1-f49.google.com with SMTP id ej4so13657316edb.7
|
||||
for <gmail@simplemail.fplante.fr>; Mon, 27 Jun 2022 08:48:15 -0700 (PDT)
|
||||
X-Gm-Message-State: AJIora8exR9DGeRFoKAtjzwLtUpH5hqx6Zt3tm8n4gUQQivGQ3fELjUV
|
||||
yT7RQIfeW9Kv2atuOcgtmGYVU4iQ8VBeLmK1xvOYL4XpXfrT7ZrJNQ==
|
||||
Authentication-Results: mx.google.com;
|
||||
dkim=pass header.i=@matera.eu header.s=fnt header.b=XahYMey7;
|
||||
dkim=pass header.i=@sendgrid.info header.s=smtpapi header.b="QOCS/yjt";
|
||||
spf=pass (google.com: domain of bounces+14445963-ab4e-csyndic.quartz=gmail.com@front-mail.matera.eu designates 168.245.4.42 as permitted sender) smtp.mailfrom="bounces+14445963-ab4e-csyndic.quartz=gmail.com@front-mail.matera.eu";
|
||||
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=matera.eu
|
||||
Received: from out.frontapp.com (unknown)
|
||||
by geopod-ismtpd-3-0 (SG)
|
||||
with ESMTP id d2gM2N7PT7W8d2-UEC4ESA
|
||||
for <csyndic.quartz@gmail.com>;
|
||||
Mon, 27 Jun 2022 15:48:11.014 +0000 (UTC)
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----sinikael-?=_1-16563448907660.10629093370416887"
|
||||
In-Reply-To:
|
||||
<imported@frontapp.com_81c5208b4cff8b0633f167fda4e6e8e8f63b7a9b>
|
||||
References:
|
||||
<imported@frontapp.com_t:AssembléeGénérale2022-06-25T16:32:03+02:006b3cdade-982b-47cd-8114-6a037dfb7d60>
|
||||
<imported@frontapp.com_f924cce139940c9935621f067d46443597394f34>
|
||||
<imported@frontapp.com_t:Appeldefonds2022-06-26T10:04:55+02:00d89f5e23-6d98-4f01-95fa-b7c7544b7aa9>
|
||||
<imported@frontapp.com_81c5208b4cff8b0633f167fda4e6e8e8f63b7a9b>
|
||||
<af07e94a66ece6564ae30a2aaac7a34c@frontapp.com>
|
||||
To: {{ alias_email }}
|
||||
Subject: Something
|
||||
Message-ID: <af07e94a66ece6564ae30a2aaac7a34c@frontapp.com>
|
||||
X-Mailer: Front (1.0; +https://frontapp.com;
|
||||
+msgid=af07e94a66ece6564ae30a2aaac7a34c@frontapp.com)
|
||||
X-Feedback-ID: 14445963:SG
|
||||
X-SG-EID:
|
||||
=?us-ascii?Q?XtlxQDg5i3HqMzQY2Upg19JPZBVl1RybInUUL2yta9uBoIU4KU1FMJ5DjWrz6g?=
|
||||
=?us-ascii?Q?fJUK5Qmneg2uc46gwp5BdHdp6Foaq5gg3xJriv3?=
|
||||
=?us-ascii?Q?9OA=2FWRifeylU9O+ngdNbOKXoeJAkROmp2mCgw9x?=
|
||||
=?us-ascii?Q?uud+EclOT9mYVtbZsydOLLm6Y2PPswQl8lnmiku?=
|
||||
=?us-ascii?Q?DAhkG15HTz2FbWGWNDFb7VrSsN5ddjAscr6sIHw?=
|
||||
=?us-ascii?Q?S48R5fnXmfhPbmlCgqFjr0FGphfuBdNAt6z6w8a?=
|
||||
=?us-ascii?Q?o9u1EYDIX7zWHZ+Tr3eyw=3D=3D?=
|
||||
X-SG-ID:
|
||||
=?us-ascii?Q?N2C25iY2uzGMFz6rgvQsb8raWjw0ZPf1VmjsCkspi=2FI9PhcvqXQTpKqqyZkvBe?=
|
||||
=?us-ascii?Q?+2RscnQ4WPkA+BN1vYgz1rezTVIqgp+rlWrKk8o?=
|
||||
=?us-ascii?Q?HoB5dzpX6HKWtWCVRi10zwlDN1+pJnySoIUrlaT?=
|
||||
=?us-ascii?Q?PA2aqQKmMQbjTl0CUAFryR8hhHcxdS0cQowZSd7?=
|
||||
=?us-ascii?Q?XNjJWLvCGF7ODwg=2FKr+4yRE8UvULS2nrdO2wWyQ?=
|
||||
=?us-ascii?Q?AiFHdPdZsRlgNomEo=3D?=
|
||||
X-Spamd-Result: default: False [-2.00 / 13.00];
|
||||
ARC_ALLOW(-1.00)[google.com:s=arc-20160816:i=1];
|
||||
MIME_GOOD(-0.10)[multipart/alternative,text/plain];
|
||||
REPLYTO_ADDR_EQ_FROM(0.00)[];
|
||||
FORGED_RECIPIENTS_FORWARDING(0.00)[];
|
||||
NEURAL_HAM(-0.00)[-0.981];
|
||||
FREEMAIL_TO(0.00)[gmail.com];
|
||||
RCVD_TLS_LAST(0.00)[];
|
||||
FREEMAIL_ENVFROM(0.00)[gmail.com];
|
||||
MIME_TRACE(0.00)[0:+,1:+,2:~];
|
||||
RWL_MAILSPIKE_POSSIBLE(0.00)[209.85.208.49:from]
|
||||
|
||||
------sinikael-?=_1-16563448907660.10629093370416887
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Hello
|
||||
------sinikael-?=_1-16563448907660.10629093370416887--
|
64
app/tests/example_emls/replacement_on_reply_phase.eml
Normal file
64
app/tests/example_emls/replacement_on_reply_phase.eml
Normal file
@ -0,0 +1,64 @@
|
||||
Received: by mail-ed1-f49.google.com with SMTP id ej4so13657316edb.7
|
||||
for <gmail@simplemail.fplante.fr>; Mon, 27 Jun 2022 08:48:15 -0700 (PDT)
|
||||
X-Gm-Message-State: AJIora8exR9DGeRFoKAtjzwLtUpH5hqx6Zt3tm8n4gUQQivGQ3fELjUV
|
||||
yT7RQIfeW9Kv2atuOcgtmGYVU4iQ8VBeLmK1xvOYL4XpXfrT7ZrJNQ==
|
||||
Authentication-Results: mx.google.com;
|
||||
dkim=pass header.i=@matera.eu header.s=fnt header.b=XahYMey7;
|
||||
dkim=pass header.i=@sendgrid.info header.s=smtpapi header.b="QOCS/yjt";
|
||||
spf=pass (google.com: domain of bounces+14445963-ab4e-csyndic.quartz=gmail.com@front-mail.matera.eu designates 168.245.4.42 as permitted sender) smtp.mailfrom="bounces+14445963-ab4e-csyndic.quartz=gmail.com@front-mail.matera.eu";
|
||||
dmarc=pass (p=NONE sp=NONE dis=NONE) header.from=matera.eu
|
||||
Received: from out.frontapp.com (unknown)
|
||||
by geopod-ismtpd-3-0 (SG)
|
||||
with ESMTP id d2gM2N7PT7W8d2-UEC4ESA
|
||||
for <csyndic.quartz@gmail.com>;
|
||||
Mon, 27 Jun 2022 15:48:11.014 +0000 (UTC)
|
||||
Content-Type: multipart/alternative;
|
||||
boundary="----sinikael-?=_1-16563448907660.10629093370416887"
|
||||
In-Reply-To:
|
||||
<imported@frontapp.com_81c5208b4cff8b0633f167fda4e6e8e8f63b7a9b>
|
||||
References:
|
||||
<imported@frontapp.com_t:AssembléeGénérale2022-06-25T16:32:03+02:006b3cdade-982b-47cd-8114-6a037dfb7d60>
|
||||
<imported@frontapp.com_f924cce139940c9935621f067d46443597394f34>
|
||||
<imported@frontapp.com_t:Appeldefonds2022-06-26T10:04:55+02:00d89f5e23-6d98-4f01-95fa-b7c7544b7aa9>
|
||||
<imported@frontapp.com_81c5208b4cff8b0633f167fda4e6e8e8f63b7a9b>
|
||||
<af07e94a66ece6564ae30a2aaac7a34c@frontapp.com>
|
||||
To: {{ contact_reply_email }}
|
||||
Subject: Something
|
||||
Message-ID: <af07e94a66ece6564ae30a2aaac7a34c@frontapp.com>
|
||||
X-Mailer: Front (1.0; +https://frontapp.com;
|
||||
+msgid=af07e94a66ece6564ae30a2aaac7a34c@frontapp.com)
|
||||
X-Feedback-ID: 14445963:SG
|
||||
X-SG-EID:
|
||||
=?us-ascii?Q?XtlxQDg5i3HqMzQY2Upg19JPZBVl1RybInUUL2yta9uBoIU4KU1FMJ5DjWrz6g?=
|
||||
=?us-ascii?Q?fJUK5Qmneg2uc46gwp5BdHdp6Foaq5gg3xJriv3?=
|
||||
=?us-ascii?Q?9OA=2FWRifeylU9O+ngdNbOKXoeJAkROmp2mCgw9x?=
|
||||
=?us-ascii?Q?uud+EclOT9mYVtbZsydOLLm6Y2PPswQl8lnmiku?=
|
||||
=?us-ascii?Q?DAhkG15HTz2FbWGWNDFb7VrSsN5ddjAscr6sIHw?=
|
||||
=?us-ascii?Q?S48R5fnXmfhPbmlCgqFjr0FGphfuBdNAt6z6w8a?=
|
||||
=?us-ascii?Q?o9u1EYDIX7zWHZ+Tr3eyw=3D=3D?=
|
||||
X-SG-ID:
|
||||
=?us-ascii?Q?N2C25iY2uzGMFz6rgvQsb8raWjw0ZPf1VmjsCkspi=2FI9PhcvqXQTpKqqyZkvBe?=
|
||||
=?us-ascii?Q?+2RscnQ4WPkA+BN1vYgz1rezTVIqgp+rlWrKk8o?=
|
||||
=?us-ascii?Q?HoB5dzpX6HKWtWCVRi10zwlDN1+pJnySoIUrlaT?=
|
||||
=?us-ascii?Q?PA2aqQKmMQbjTl0CUAFryR8hhHcxdS0cQowZSd7?=
|
||||
=?us-ascii?Q?XNjJWLvCGF7ODwg=2FKr+4yRE8UvULS2nrdO2wWyQ?=
|
||||
=?us-ascii?Q?AiFHdPdZsRlgNomEo=3D?=
|
||||
X-Spamd-Result: default: False [-2.00 / 13.00];
|
||||
ARC_ALLOW(-1.00)[google.com:s=arc-20160816:i=1];
|
||||
MIME_GOOD(-0.10)[multipart/alternative,text/plain];
|
||||
REPLYTO_ADDR_EQ_FROM(0.00)[];
|
||||
FORGED_RECIPIENTS_FORWARDING(0.00)[];
|
||||
NEURAL_HAM(-0.00)[-0.981];
|
||||
FREEMAIL_TO(0.00)[gmail.com];
|
||||
RCVD_TLS_LAST(0.00)[];
|
||||
FREEMAIL_ENVFROM(0.00)[gmail.com];
|
||||
MIME_TRACE(0.00)[0:+,1:+,2:~];
|
||||
RWL_MAILSPIKE_POSSIBLE(0.00)[209.85.208.49:from]
|
||||
|
||||
------sinikael-?=_1-16563448907660.10629093370416887
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Contact is {{ contact_reply_email }}
|
||||
Other contact is {{ other_contact_reply_email }}
|
||||
------sinikael-?=_1-16563448907660.10629093370416887--
|
157
app/tests/example_emls/yahoo_complaint.eml
Normal file
157
app/tests/example_emls/yahoo_complaint.eml
Normal file
@ -0,0 +1,157 @@
|
||||
X-SimpleLogin-Client-IP: 66.163.186.21
|
||||
Received-SPF: None (mailfrom) identity=mailfrom; client-ip=66.163.186.21;
|
||||
helo=sonic326-46.consmr.mail.ne1.yahoo.com;
|
||||
envelope-from=feedback@arf.mail.yahoo.com; receiver=<UNKNOWN>
|
||||
Received: from sonic326-46.consmr.mail.ne1.yahoo.com
|
||||
(sonic326-46.consmr.mail.ne1.yahoo.com [66.163.186.21])
|
||||
(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits)
|
||||
key-exchange ECDHE (P-256) server-signature RSA-PSS (2048 bits)
|
||||
server-digest SHA256)
|
||||
(No client certificate requested)
|
||||
by prod4.simplelogin.co (Postfix) with ESMTPS id 160E19C47C
|
||||
for <{{ postmaster }}>; Sun, 8 May 2022 13:31:32 +0000 (UTC)
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=arf.mail.yahoo.com;
|
||||
s=arf; t=1652016690; bh=y3TXlG8d2nUmz+Mm6gBEX1p1y2rwlM+LRC89Bp+HwGo=;
|
||||
h=Date:From:To:Subject:From:Subject:Reply-To;
|
||||
b=HyuY58LSzfkdH9FynjNWEl6QJeeImKRbIzrnR64sY/ggFD6fF9w1/fpXDmJ8RHpB/72llGb8nkVJkn/TK+adBCZvw4Y0SC2m8qbn6BdaC5kvAWkN6VUxvQWFMWTptAmeX+UUxY2hjEXLZQwNUd4nvvhZkbdyzw5wFSpYX0hnxAA=
|
||||
X-SONIC-DKIM-SIGN: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yahoo.com; s=s2048;
|
||||
t=1652016690; bh=0SlXAOx+1D8SxkBJpASrTwUGjphtzchFZOSJr0X+U2m=;
|
||||
h=X-Sonic-MF:Date:From:To:Subject:From:Subject;
|
||||
b=smqcDrz5jxsmGycWk9tNncLBjcQIqBnZmsQzkJ6g8fyhQw2e30y05iTnsOBTr0S9qTPK3I2JBv0P73TH7vDAnZAnaewzj9Dymw7Z+UxXKdrPBf/tD8RGw9cX6C0eb7GUjHvbvXS03IkSGnvOPPCXLsTDXYOTflcU7A0A2L+cS9ogEBl/4AFwBf/z+lcMH20h2dZ6+wPtqPCgRY1Hf45cv4gfHrFG0a18n3BBq0doCA4cRTXeeuv06fqsUCk2GF6z0mm3YWu+umcUs16QmgjHKhy4SJHvTZfx4zFBxQEOM3hvBzriL5g0D3Rg71CdkI8TVqsyXS1YWVSQFakAw0hM+A==
|
||||
X-Sonic-MF: feedback@arf.mail.yahoo.com
|
||||
Received: from sonic.gate.mail.ne1.yahoo.com by
|
||||
sonic326.consmr.mail.ne1.yahoo.com with HTTP; Sun, 8 May 2022 13:31:30 +0000
|
||||
Date: Sun, 8 May 2022 13:31:28 +0000 (UTC)
|
||||
From: Yahoo! Mail AntiSpam Feedback <feedback@arf.mail.yahoo.com>
|
||||
To: {{ postmaster }}
|
||||
Message-ID:
|
||||
<1486688083.18136997.1652016688605@chakraconsumer2.asd.mail.ne1.yahoo.com>
|
||||
Subject: Original subject
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type=feedback-report;
|
||||
boundary="----=_Part_18136996_1734597748.1652016688604"
|
||||
X-Yahoo-Newman-Property: cfl
|
||||
X-Yahoo-Newman-Id: cfl-test
|
||||
X-Spamd-Result: default: False [-0.65 / 13.00];
|
||||
DMARC_POLICY_ALLOW(-0.50)[yahoo.com,reject];
|
||||
R_DKIM_ALLOW(-0.20)[arf.mail.yahoo.com:s=arf];
|
||||
SUBJ_ALL_CAPS(0.15)[2];
|
||||
MIME_GOOD(-0.10)[text/plain,multipart/alternative];
|
||||
R_SPF_NA(0.00)[no SPF record];
|
||||
FROM_EQ_ENVFROM(0.00)[];
|
||||
MIME_TRACE(0.00)[0:~,1:+,2:~,3:+,4:~,5:+,6:+,7:~];
|
||||
RCVD_TLS_LAST(0.00)[];
|
||||
RCVD_IN_DNSWL_NONE(0.00)[66.163.186.21:from];
|
||||
ASN(0.00)[asn:36646, ipnet:66.163.184.0/21, country:US];
|
||||
ARC_NA(0.00)[];
|
||||
DKIM_TRACE(0.00)[arf.mail.yahoo.com:+];
|
||||
MID_RHS_MATCH_FROMTLD(0.00)[];
|
||||
TO_MATCH_ENVRCPT_ALL(0.00)[];
|
||||
FROM_HAS_DN(0.00)[];
|
||||
RCVD_COUNT_TWO(0.00)[2];
|
||||
TO_DN_NONE(0.00)[];
|
||||
RCPT_COUNT_ONE(0.00)[1];
|
||||
NEURAL_SPAM(0.00)[0.429];
|
||||
DWL_DNSWL_NONE(0.00)[yahoo.com:dkim]
|
||||
X-Rspamd-Queue-Id: 160E19C47C
|
||||
X-Rspamd-Server: prod4
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
------=_Part_18136996_1734597748.1652016688604
|
||||
Content-Type: text/plain; charset=us-ascii
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: inline
|
||||
|
||||
This is an email abuse report for an email message from simplelogin.co on Sun, 8 May 2022 11:12:35 +0000
|
||||
|
||||
------=_Part_18136996_1734597748.1652016688604
|
||||
Content-Type: message/feedback-report
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Disposition: inline
|
||||
|
||||
Feedback-Type: abuse
|
||||
User-Agent: Yahoo!-Mail-Feedback/2.0
|
||||
Version: 0.1
|
||||
Original-Mail-From:
|
||||
<{{ return_path }}>
|
||||
Original-Rcpt-To: {{ rcpt }}
|
||||
Received-Date: Sun, 8 May 2022 11:12:35 +0000
|
||||
Reported-Domain: simplelogin.co
|
||||
Authentication-Results: authentication result string is not available
|
||||
|
||||
|
||||
------=_Part_18136996_1734597748.1652016688604
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline
|
||||
|
||||
Received: from 10.217.151.74
|
||||
by atlas316.free.mail.ne1.yahoo.com with HTTPS;
|
||||
Sun, 8 May 2022 11:12:34 +0000
|
||||
Return-Path:
|
||||
<{{ return_path }}>
|
||||
X-Originating-Ip: [176.129.238.160]
|
||||
Received-SPF: pass (domain of simplelogin.co designates 176.119.200.160 as
|
||||
permitted sender)
|
||||
Authentication-Results: atlas316.free.mail.ne1.yahoo.com;
|
||||
dkim=pass header.i=@simplelogin.co header.s=dkim;
|
||||
spf=pass smtp.mailfrom=simplelogin.co;
|
||||
dmarc=pass(p=QUARANTINE) header.from=simplelogin.co;
|
||||
X-Apparently-To: syn_flood91@yahoo.com; Sun, 8 May 2022 11:12:35 +0000
|
||||
X-YMailISG: 5XbMksQWLDvXV9CBjagtqIT6OTC44ku5XiuZJQp_W6hhWfR.
|
||||
.wUIhFV6vRR_JeMUxC0ZAvugteAP2pe.bqk06ovvYnhJMg_HTvcmfVltbWxQ
|
||||
tK7xNSs8D2PWQdyDDzB3rdFdIIfSrQnDTGjP2xpTAqLQk3IXSuUBX7s4f8uA
|
||||
WUELPWj36_Xtqrwyj.ya4Ezw_ePzPhZGmMdCsbz2H5Jh45TLbk5HhL.TDDbH
|
||||
9Dz__HKLUC8acH0hu1vrPvo1ljzwbl_0cqlj10qMIChpB51XVDtyNA_WgWvE
|
||||
QL1hFHS0tScfRT0xATM8w8FJv1eA0ODjakDtTRgmaWBTphzeoR.FyTBj14y5
|
||||
burx6lkUqipfP7UZpNmcNDYHQdTEmdGa8JDZMX.lpM5IMOhkByIQuoTN4.Cx
|
||||
8qz9kb.o0DqxqNRgn4_fRRAoSn1xejDbzZMu.SWSvJ1KJwAfLtep37ISqNKl
|
||||
yeBeDJFMnHUjRD8B2wBB46zq4ngHFWjBGkAGQVBssLzj594FXg13aO.TnJU7
|
||||
WJ_cUSzoaH9HjgYDTi4.1x68jVxpZIEdhDe7pjLCUL2ugWdar9S7pFlyKWfa
|
||||
iTH8yQ10NXtLCwGpJ.0kgZH2WXJgyJmrq0a3j63skib7WJYtKOXfsbHV8b9e
|
||||
WxClOETCe03PtdD6G2sjEJSNFyTH_Qzzq6_21PO6kjmnEnBbibAnkiJbGhIJ
|
||||
kOSqyp_vFqstpd38vtt7iLI8L3PkyZDQXS0hB1ZCOsZqBDGJXAoWFRBtxMSd
|
||||
rMVkdvB6r8xJtn.1JrV1hpX4yRbCuEnCCPcwtGamlpyq5LG6YanKUVB868KF
|
||||
UuZ4AHFwi.m_FYHalwtfCaArtWzYybl2nQQLjPbnXxqNvfwKt3ATKFEO40ZV
|
||||
w1Ri7y.cO__09.eQHKIUNgMNeWgt.luD3thsEl0yz_ThzrCEkXDB1xAPNnLV
|
||||
tb03RulEB0xNauYTuWgKR8WJzkO4LuXMlzNAAYBQLQy_t0GoezAs7Z4oq.CH
|
||||
EfTK88cDJ7j7dXcXBi7q6g1NBZT3tyd9Bfn2DVdFaWAjWV9Lb8tir6J43MDP
|
||||
byTrZ_zJxTWKgafhOxL0gZbd5xIEZ1eHHeQO5pVZlN6FR1awozFgS4NcZu5u
|
||||
5qRtn6zHo3zNe9ORwwxqlHAEJR_5I09WYSdmTxh2QkkDQLjSlwUNV4K8jxdH
|
||||
L4ePIzNCQCt_bsGoG3uPXl8jtPD4sUWGY1lCeKAm.AHgZ.pSXXypMUpq4y14
|
||||
NihY89H61y5ZXo4Zd77shda_
|
||||
Received: from 176.119.200.160 (EHLO mail-200160.simplelogin.co)
|
||||
by 10.217.151.74 with SMTPs
|
||||
(version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256);
|
||||
Sun, 08 May 2022 11:12:34 +0000
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=simplelogin.co;
|
||||
s=dkim; t=1652008349; h=From:To:Subject:Message-ID:Date;
|
||||
bh=9HnrBUpZUe8OSXqTw1qF667IwLtHI8DqiyD0yAovIO4=;
|
||||
b=PsxiMydvEQveb20xgUvvq3DhxlLyqqoPW7sC8d/pAm8tj7T2O+7z5xxR6vVbgz823Bglzc
|
||||
djb3pRvNLgHnTozC+FiFOF8nVlWGybosn5oRfmNGkF9bhr0bJmfcDhiuC/tOaZKkod2lbf
|
||||
jQ8bqMZhCsN/xVpkMqJdNJefdkj3dP4=
|
||||
MIME-Version: 1.0
|
||||
Date: Sun, 8 May 2022 04:11:42 -0700
|
||||
Message-ID:
|
||||
<CAKGh96GHg2kuwvm4biQ-PF-4-8SPZ6JyPj-=GpoYZ6njctoRtg@mail.gmail.com>
|
||||
Subject: MF
|
||||
Content-Type: multipart/alternative; boundary="0000000000006dd95f05de7e2a70"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
X-SimpleLogin-Type: Forward
|
||||
X-SimpleLogin-EmailLog-ID: 41263490
|
||||
X-SimpleLogin-Envelope-From: {{ sender }}
|
||||
X-SimpleLogin-Envelope-To: {{ rcpt }}
|
||||
From: {{ sender }}
|
||||
To: {{ rcpt_comma_list }}
|
||||
List-Unsubscribe: <mailto:unsubscribe@simplelogin.co?subject=1231546=>
|
||||
X-SimpleLogin-Want-Signing: yes
|
||||
Content-Length: 473
|
||||
|
||||
--0000000000006dd95f05de7e2a70
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
|
||||
Here goes the original email content
|
||||
|
||||
--0000000000006dd95f05de7e2a70--
|
||||
|
||||
|
||||
------=_Part_18136996_1734597748.1652016688604--
|
0
app/tests/handler/__init__.py
Normal file
0
app/tests/handler/__init__.py
Normal file
111
app/tests/handler/test_provider_complaints.py
Normal file
111
app/tests/handler/test_provider_complaints.py
Normal file
@ -0,0 +1,111 @@
|
||||
import random
|
||||
from email.message import Message
|
||||
|
||||
import pytest
|
||||
from app.config import (
|
||||
ALERT_COMPLAINT_FORWARD_PHASE,
|
||||
ALERT_COMPLAINT_REPLY_PHASE,
|
||||
ALERT_COMPLAINT_TRANSACTIONAL_PHASE,
|
||||
POSTMASTER,
|
||||
)
|
||||
from app.db import Session
|
||||
from app.email_utils import generate_verp_email
|
||||
from app.handler.provider_complaint import (
|
||||
handle_hotmail_complaint,
|
||||
handle_yahoo_complaint,
|
||||
)
|
||||
from app.mail_sender import mail_sender
|
||||
from app.models import (
|
||||
Alias,
|
||||
ProviderComplaint,
|
||||
SentAlert,
|
||||
EmailLog,
|
||||
VerpType,
|
||||
Contact,
|
||||
)
|
||||
from tests.utils import create_new_user, load_eml_file
|
||||
|
||||
origins = [
|
||||
[handle_yahoo_complaint, "yahoo"],
|
||||
[handle_hotmail_complaint, "hotmail"],
|
||||
]
|
||||
|
||||
|
||||
def prepare_complaint(
|
||||
provider_name: str, alias: Alias, rcpt_address: str, sender_address: str
|
||||
) -> Message:
|
||||
contact = Contact.create(
|
||||
user_id=alias.user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"contact{random.random()}@mailbox.test",
|
||||
reply_email="d@e.f",
|
||||
commit=True,
|
||||
)
|
||||
elog = EmailLog.create(
|
||||
user_id=alias.user.id,
|
||||
mailbox_id=alias.user.default_mailbox_id,
|
||||
contact_id=contact.id,
|
||||
commit=True,
|
||||
bounced=True,
|
||||
)
|
||||
return_path = generate_verp_email(VerpType.bounce_forward, elog.id)
|
||||
return load_eml_file(
|
||||
f"{provider_name}_complaint.eml",
|
||||
{
|
||||
"postmaster": POSTMASTER,
|
||||
"return_path": return_path,
|
||||
"rcpt": rcpt_address,
|
||||
"sender": sender_address,
|
||||
"rcpt_comma_list": f"{rcpt_address},other_rcpt@somwhere.net",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
@pytest.mark.parametrize("handle_ftor,provider", origins)
|
||||
def test_provider_to_user(flask_client, handle_ftor, provider):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
complaint = prepare_complaint(provider, alias, user.email, "nobody@nowhere.net")
|
||||
assert handle_ftor(complaint)
|
||||
found = ProviderComplaint.filter_by(user_id=user.id).all()
|
||||
assert len(found) == 0
|
||||
alerts = SentAlert.filter_by(user_id=user.id).all()
|
||||
assert len(alerts) == 1
|
||||
sent_mails = mail_sender.get_stored_emails()
|
||||
assert len(sent_mails) == 1
|
||||
assert alerts[0].alert_type == f"{ALERT_COMPLAINT_TRANSACTIONAL_PHASE}_{provider}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("handle_ftor,provider", origins)
|
||||
def test_provider_forward_phase(flask_client, handle_ftor, provider):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
complaint = prepare_complaint(provider, alias, "nobody@nowhere.net", alias.email)
|
||||
assert handle_ftor(complaint)
|
||||
found = ProviderComplaint.filter_by(user_id=user.id).all()
|
||||
assert len(found) == 1
|
||||
alerts = SentAlert.filter_by(user_id=user.id).all()
|
||||
assert len(alerts) == 1
|
||||
assert alerts[0].alert_type == f"{ALERT_COMPLAINT_REPLY_PHASE}_{provider}"
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
@pytest.mark.parametrize("handle_ftor,provider", origins)
|
||||
def test_provider_reply_phase(flask_client, handle_ftor, provider):
|
||||
mail_sender.store_emails_instead_of_sending()
|
||||
mail_sender.purge_stored_emails()
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
complaint = prepare_complaint(provider, alias, alias.email, "no@no.no")
|
||||
assert handle_ftor(complaint)
|
||||
found = ProviderComplaint.filter_by(user_id=user.id).all()
|
||||
assert len(found) == 0
|
||||
alerts = SentAlert.filter_by(user_id=user.id).all()
|
||||
assert len(alerts) == 1
|
||||
sent_mails = mail_sender.get_stored_emails()
|
||||
assert len(sent_mails) == 1
|
||||
assert alerts[0].alert_type == f"{ALERT_COMPLAINT_FORWARD_PHASE}_{provider}"
|
46
app/tests/handler/test_spamd_result.py
Normal file
46
app/tests/handler/test_spamd_result.py
Normal file
@ -0,0 +1,46 @@
|
||||
from app.handler.spamd_result import DmarcCheckResult, SpamdResult
|
||||
from tests.utils import load_eml_file
|
||||
|
||||
|
||||
def test_dmarc_result_softfail():
|
||||
msg = load_eml_file("dmarc_gmail_softfail.eml")
|
||||
assert DmarcCheckResult.soft_fail == SpamdResult.extract_from_headers(msg).dmarc
|
||||
assert SpamdResult.extract_from_headers(msg).rspamd_score == 0.5
|
||||
|
||||
|
||||
def test_dmarc_result_quarantine():
|
||||
msg = load_eml_file("dmarc_quarantine.eml")
|
||||
assert DmarcCheckResult.quarantine == SpamdResult.extract_from_headers(msg).dmarc
|
||||
|
||||
|
||||
def test_dmarc_result_reject():
|
||||
msg = load_eml_file("dmarc_reject.eml")
|
||||
assert DmarcCheckResult.reject == SpamdResult.extract_from_headers(msg).dmarc
|
||||
|
||||
|
||||
def test_dmarc_result_allow():
|
||||
msg = load_eml_file("dmarc_allow.eml")
|
||||
assert DmarcCheckResult.allow == SpamdResult.extract_from_headers(msg).dmarc
|
||||
|
||||
|
||||
def test_dmarc_result_na():
|
||||
msg = load_eml_file("dmarc_na.eml")
|
||||
assert DmarcCheckResult.not_available == SpamdResult.extract_from_headers(msg).dmarc
|
||||
|
||||
|
||||
def test_dmarc_result_bad_policy():
|
||||
msg = load_eml_file("dmarc_bad_policy.eml")
|
||||
assert SpamdResult._get_from_message(msg) is None
|
||||
assert DmarcCheckResult.bad_policy == SpamdResult.extract_from_headers(msg).dmarc
|
||||
assert SpamdResult._get_from_message(msg) is not None
|
||||
|
||||
|
||||
def test_parse_rspamd_score():
|
||||
msg = load_eml_file("dmarc_gmail_softfail.eml")
|
||||
assert SpamdResult.extract_from_headers(msg).rspamd_score == 0.5
|
||||
|
||||
|
||||
def test_cannot_parse_rspamd_score():
|
||||
msg = load_eml_file("dmarc_cannot_parse_rspamd_score.eml")
|
||||
# use the default score when cannot parse
|
||||
assert SpamdResult.extract_from_headers(msg).rspamd_score == -1
|
116
app/tests/handler/test_unsubscribe_encoder.py
Normal file
116
app/tests/handler/test_unsubscribe_encoder.py
Normal file
@ -0,0 +1,116 @@
|
||||
import pytest
|
||||
|
||||
from app import config
|
||||
from app.handler.unsubscribe_encoder import (
|
||||
UnsubscribeData,
|
||||
UnsubscribeAction,
|
||||
UnsubscribeEncoder,
|
||||
UnsubscribeOriginalData,
|
||||
)
|
||||
|
||||
legacy_subject_test_data = [
|
||||
("3=", UnsubscribeData(UnsubscribeAction.DisableAlias, 3)),
|
||||
("438_", UnsubscribeData(UnsubscribeAction.DisableContact, 438)),
|
||||
("4325*", UnsubscribeData(UnsubscribeAction.UnsubscribeNewsletter, 4325)),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_subject, expected_deco", legacy_subject_test_data)
|
||||
def test_legacy_unsub_subject(expected_subject, expected_deco):
|
||||
info = UnsubscribeEncoder.decode_subject(expected_subject)
|
||||
assert info == expected_deco
|
||||
|
||||
|
||||
legacy_url_test_data = [
|
||||
(
|
||||
f"{config.URL}/dashboard/unsubscribe/3",
|
||||
UnsubscribeData(UnsubscribeAction.DisableAlias, 3),
|
||||
),
|
||||
(
|
||||
f"{config.URL}/dashboard/block_contact/5",
|
||||
UnsubscribeData(UnsubscribeAction.DisableContact, 5),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_url, unsub_data", legacy_url_test_data)
|
||||
def test_encode_decode_unsub_subject(expected_url, unsub_data):
|
||||
url = UnsubscribeEncoder.encode_url(unsub_data.action, unsub_data.data)
|
||||
assert expected_url == url
|
||||
|
||||
|
||||
legacy_mail_or_link_test_data = [
|
||||
(
|
||||
f"{config.URL}/dashboard/unsubscribe/3",
|
||||
False,
|
||||
UnsubscribeData(UnsubscribeAction.DisableAlias, 3),
|
||||
),
|
||||
(
|
||||
"mailto:me@nowhere.net?subject=un.WzIsIDld.ONeJMiTW6CosJg4PMR1MPcDs-6GWoTOQFMfA2A",
|
||||
True,
|
||||
UnsubscribeData(UnsubscribeAction.DisableAlias, 9),
|
||||
),
|
||||
(
|
||||
f"{config.URL}/dashboard/block_contact/8",
|
||||
False,
|
||||
UnsubscribeData(UnsubscribeAction.DisableContact, 8),
|
||||
),
|
||||
(
|
||||
"mailto:me@nowhere.net?subject=un.WzMsIDhd.eo_Ynk0eNyPtsHXMpTqw7HMFgYmm1Up_wWUc3g",
|
||||
True,
|
||||
UnsubscribeData(UnsubscribeAction.DisableContact, 8),
|
||||
),
|
||||
(
|
||||
"mailto:me@nowhere.net?subject=un.WzEsIDgzXQ.NZAWqfpCmLEszwc5nWuQwDSLJ3TXO3rcOe_73Q",
|
||||
True,
|
||||
UnsubscribeData(UnsubscribeAction.UnsubscribeNewsletter, 83),
|
||||
),
|
||||
(
|
||||
f"{config.URL}/dashboard/unsubscribe/encoded?data=un.WzQsIFswLCAxLCAiYUBiLmMiLCAic3ViamVjdCJdXQ.aU3T5XNzJIG4LDm6-pqJk4vxxJxpgVYzc9MEFQ",
|
||||
False,
|
||||
UnsubscribeData(
|
||||
UnsubscribeAction.OriginalUnsubscribeMailto,
|
||||
UnsubscribeOriginalData(1, "a@b.c", "subject"),
|
||||
),
|
||||
),
|
||||
(
|
||||
"mailto:me@nowhere.net?subject=un.WzQsIFswLCAxLCAiYUBiLmMiLCAic3ViamVjdCJdXQ.aU3T5XNzJIG4LDm6-pqJk4vxxJxpgVYzc9MEFQ",
|
||||
True,
|
||||
UnsubscribeData(
|
||||
UnsubscribeAction.OriginalUnsubscribeMailto,
|
||||
UnsubscribeOriginalData(1, "a@b.c", "subject"),
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"expected_link, via_mail, unsub_data", legacy_mail_or_link_test_data
|
||||
)
|
||||
def test_encode_legacy_link(expected_link, via_mail, unsub_data):
|
||||
if via_mail:
|
||||
config.UNSUBSCRIBER = "me@nowhere.net"
|
||||
else:
|
||||
config.UNSUBSCRIBER = None
|
||||
link_info = UnsubscribeEncoder.encode(unsub_data.action, unsub_data.data)
|
||||
assert via_mail == link_info.via_email
|
||||
assert expected_link == link_info.link
|
||||
|
||||
|
||||
encode_decode_test_data = [
|
||||
UnsubscribeData(UnsubscribeAction.DisableContact, 3),
|
||||
UnsubscribeData(UnsubscribeAction.DisableContact, 10),
|
||||
UnsubscribeData(UnsubscribeAction.DisableAlias, 101),
|
||||
UnsubscribeData(
|
||||
UnsubscribeAction.OriginalUnsubscribeMailto,
|
||||
UnsubscribeOriginalData(323, "a@b.com", "some subject goes here"),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unsub_data", encode_decode_test_data)
|
||||
def test_encode_decode_unsub(unsub_data):
|
||||
encoded = UnsubscribeEncoder.encode_subject(unsub_data.action, unsub_data.data)
|
||||
decoded = UnsubscribeEncoder.decode_subject(encoded)
|
||||
assert unsub_data.action == decoded.action
|
||||
assert unsub_data.data == decoded.data
|
206
app/tests/handler/test_unsubscribe_generator.py
Normal file
206
app/tests/handler/test_unsubscribe_generator.py
Normal file
@ -0,0 +1,206 @@
|
||||
from email.message import Message
|
||||
from typing import Iterable
|
||||
|
||||
import pytest
|
||||
|
||||
from app import config
|
||||
from app.db import Session
|
||||
from app.email import headers
|
||||
from app.handler.unsubscribe_encoder import (
|
||||
UnsubscribeAction,
|
||||
UnsubscribeEncoder,
|
||||
UnsubscribeOriginalData,
|
||||
)
|
||||
from app.handler.unsubscribe_generator import UnsubscribeGenerator
|
||||
from app.models import Alias, Contact, UnsubscribeBehaviourEnum
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
TEST_UNSUB_EMAIL = "unsub@sl.com"
|
||||
|
||||
|
||||
def generate_unsub_block_contact_data() -> Iterable:
|
||||
user = create_new_user()
|
||||
user.unsub_behaviour = UnsubscribeBehaviourEnum.BlockContact
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
subject = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.DisableContact, contact.id
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
True,
|
||||
"<https://lol.com>, <mailto:somewhere@not.net>",
|
||||
f"<mailto:{TEST_UNSUB_EMAIL}?subject={subject}>",
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
False,
|
||||
"<https://lol.com>, <mailto:somewhere@not.net>",
|
||||
f"<{config.URL}/dashboard/block_contact/{contact.id}>",
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
False,
|
||||
None,
|
||||
f"<{config.URL}/dashboard/block_contact/{contact.id}>",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alias_id, contact_id, unsub_via_mail, original_header, expected_header",
|
||||
generate_unsub_block_contact_data(),
|
||||
)
|
||||
def test_unsub_disable_contact(
|
||||
alias_id, contact_id, unsub_via_mail, original_header, expected_header
|
||||
):
|
||||
alias = Alias.get(alias_id)
|
||||
contact = Contact.get(contact_id)
|
||||
config.UNSUBSCRIBER = TEST_UNSUB_EMAIL if unsub_via_mail else None
|
||||
message = Message()
|
||||
message[headers.LIST_UNSUBSCRIBE] = original_header
|
||||
message = UnsubscribeGenerator().add_header_to_message(alias, contact, message)
|
||||
assert expected_header == message[headers.LIST_UNSUBSCRIBE]
|
||||
if not expected_header or expected_header.find("<http") == -1:
|
||||
assert message[headers.LIST_UNSUBSCRIBE_POST] is None
|
||||
else:
|
||||
assert "List-Unsubscribe=One-Click" == message[headers.LIST_UNSUBSCRIBE_POST]
|
||||
|
||||
|
||||
def generate_unsub_disable_alias_data() -> Iterable:
|
||||
user = create_new_user()
|
||||
user.unsub_behaviour = UnsubscribeBehaviourEnum.DisableAlias
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
subject = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.DisableAlias, alias.id
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
True,
|
||||
"<https://lol.com>, <mailto:somewhere@not.net>",
|
||||
f"<mailto:{TEST_UNSUB_EMAIL}?subject={subject}>",
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
False,
|
||||
"<https://lol.com>, <mailto:somewhere@not.net>",
|
||||
f"<{config.URL}/dashboard/unsubscribe/{alias.id}>",
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
False,
|
||||
None,
|
||||
f"<{config.URL}/dashboard/unsubscribe/{alias.id}>",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alias_id, contact_id, unsub_via_mail, original_header, expected_header",
|
||||
generate_unsub_disable_alias_data(),
|
||||
)
|
||||
def test_unsub_disable_alias(
|
||||
alias_id, contact_id, unsub_via_mail, original_header, expected_header
|
||||
):
|
||||
alias = Alias.get(alias_id)
|
||||
contact = Contact.get(contact_id)
|
||||
config.UNSUBSCRIBER = TEST_UNSUB_EMAIL if unsub_via_mail else None
|
||||
message = Message()
|
||||
message[headers.LIST_UNSUBSCRIBE] = original_header
|
||||
message = UnsubscribeGenerator().add_header_to_message(alias, contact, message)
|
||||
assert expected_header == message[headers.LIST_UNSUBSCRIBE]
|
||||
if not expected_header or expected_header.find("<http") == -1:
|
||||
assert message[headers.LIST_UNSUBSCRIBE_POST] is None
|
||||
else:
|
||||
assert "List-Unsubscribe=One-Click" == message[headers.LIST_UNSUBSCRIBE_POST]
|
||||
|
||||
|
||||
def generate_unsub_preserve_original_data() -> Iterable:
|
||||
user = create_new_user()
|
||||
user.unsub_behaviour = UnsubscribeBehaviourEnum.PreserveOriginal
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
True,
|
||||
"<https://lol.com>, <mailto:somewhere@not.net>",
|
||||
"<https://lol.com>",
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
False,
|
||||
"<https://lol.com>, <mailto:somewhere@not.net>",
|
||||
"<https://lol.com>",
|
||||
)
|
||||
unsub_data = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.OriginalUnsubscribeMailto,
|
||||
UnsubscribeOriginalData(alias.id, "test@test.com", "hello"),
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
True,
|
||||
"<mailto:test@test.com?subject=hello>",
|
||||
f"<mailto:{TEST_UNSUB_EMAIL}?subject={unsub_data}>",
|
||||
)
|
||||
yield (
|
||||
alias.id,
|
||||
contact.id,
|
||||
False,
|
||||
"<mailto:test@test.com?subject=hello>",
|
||||
f"<{config.URL}/dashboard/unsubscribe/encoded?data={unsub_data}>",
|
||||
)
|
||||
yield (alias.id, contact.id, True, None, None)
|
||||
yield (alias.id, contact.id, False, None, None)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"alias_id, contact_id, unsub_via_mail, original_header, expected_header",
|
||||
generate_unsub_preserve_original_data(),
|
||||
)
|
||||
def test_unsub_preserve_original(
|
||||
alias_id, contact_id, unsub_via_mail, original_header, expected_header
|
||||
):
|
||||
alias = Alias.get(alias_id)
|
||||
contact = Contact.get(contact_id)
|
||||
config.UNSUBSCRIBER = TEST_UNSUB_EMAIL if unsub_via_mail else None
|
||||
message = Message()
|
||||
message[headers.LIST_UNSUBSCRIBE] = original_header
|
||||
message = UnsubscribeGenerator().add_header_to_message(alias, contact, message)
|
||||
assert expected_header == message[headers.LIST_UNSUBSCRIBE]
|
||||
if not expected_header or expected_header.find("<http") == -1:
|
||||
assert message[headers.LIST_UNSUBSCRIBE_POST] is None
|
||||
else:
|
||||
assert "List-Unsubscribe=One-Click" == message[headers.LIST_UNSUBSCRIBE_POST]
|
231
app/tests/handler/test_unsubscribe_handler.py
Normal file
231
app/tests/handler/test_unsubscribe_handler.py
Normal file
@ -0,0 +1,231 @@
|
||||
from email.message import Message
|
||||
from random import random
|
||||
|
||||
from aiosmtpd.smtp import Envelope
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.email import headers, status
|
||||
from app.email_utils import parse_full_address
|
||||
from app.handler.unsubscribe_encoder import (
|
||||
UnsubscribeEncoder,
|
||||
UnsubscribeAction,
|
||||
UnsubscribeOriginalData,
|
||||
)
|
||||
from app.handler.unsubscribe_handler import (
|
||||
UnsubscribeHandler,
|
||||
)
|
||||
from app.mail_sender import mail_sender
|
||||
from app.models import Alias, Contact, User
|
||||
from tests.utils import create_new_user, login
|
||||
|
||||
|
||||
def _get_envelope_and_message(user: User, subject: str) -> (Envelope, Message):
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = user.email
|
||||
message = Message()
|
||||
message[headers.SUBJECT] = subject
|
||||
return envelope, message
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_old_subject_disable_alias():
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
envelope, message = _get_envelope_and_message(user, f"{alias.id}=")
|
||||
response = UnsubscribeHandler().handle_unsubscribe_from_message(envelope, message)
|
||||
assert status.E202 == response
|
||||
assert not Alias.get(alias.id).enabled
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_old_subject_block_contact():
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email=f"{random()}@sl.local",
|
||||
block_forward=False,
|
||||
commit=True,
|
||||
)
|
||||
envelope, message = _get_envelope_and_message(user, f"{contact.id}_")
|
||||
response = UnsubscribeHandler().handle_unsubscribe_from_message(envelope, message)
|
||||
assert status.E202 == response
|
||||
assert Contact.get(contact.id).block_forward
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_old_subject_disable_newsletter():
|
||||
user = create_new_user()
|
||||
envelope, message = _get_envelope_and_message(user, f"{user.id}*")
|
||||
response = UnsubscribeHandler().handle_unsubscribe_from_message(envelope, message)
|
||||
assert status.E202 == response
|
||||
assert not User.get(user.id).notification
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_new_subject_disable_alias():
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
header = UnsubscribeEncoder.encode_subject(UnsubscribeAction.DisableAlias, alias.id)
|
||||
envelope, message = _get_envelope_and_message(user, header)
|
||||
response = UnsubscribeHandler().handle_unsubscribe_from_message(envelope, message)
|
||||
assert status.E202 == response
|
||||
assert not Alias.get(alias.id).enabled
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_new_subject_block_contact():
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email=f"{random()}@sl.local",
|
||||
block_forward=False,
|
||||
commit=True,
|
||||
)
|
||||
header = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.DisableContact, contact.id
|
||||
)
|
||||
envelope, message = _get_envelope_and_message(user, header)
|
||||
response = UnsubscribeHandler().handle_unsubscribe_from_message(envelope, message)
|
||||
assert status.E202 == response
|
||||
assert Contact.get(contact.id).block_forward
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_new_subject_disable_newsletter():
|
||||
user = create_new_user()
|
||||
header = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.UnsubscribeNewsletter, user.id
|
||||
)
|
||||
envelope, message = _get_envelope_and_message(user, header)
|
||||
response = UnsubscribeHandler().handle_unsubscribe_from_message(envelope, message)
|
||||
assert status.E202 == response
|
||||
assert not User.get(user.id).notification
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_new_subject_original_unsub():
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = user.email
|
||||
message = Message()
|
||||
original_recipient = f"{random()}@out.com"
|
||||
original_subject = f"Unsubsomehow{random()}"
|
||||
message[headers.SUBJECT] = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.OriginalUnsubscribeMailto,
|
||||
UnsubscribeOriginalData(alias.id, original_recipient, original_subject),
|
||||
)
|
||||
response = UnsubscribeHandler().handle_unsubscribe_from_message(envelope, message)
|
||||
assert status.E202 == response
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
mail_sent = mail_sender.get_stored_emails()[0]
|
||||
assert mail_sent.envelope_to == original_recipient
|
||||
name, address = parse_full_address(mail_sent.msg[headers.FROM])
|
||||
assert name == ""
|
||||
assert alias.email == address
|
||||
assert mail_sent.msg[headers.TO] == original_recipient
|
||||
assert mail_sent.msg[headers.SUBJECT] == original_subject
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_request_disable_alias(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
req_data = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.DisableAlias, alias.id
|
||||
)
|
||||
|
||||
req = flask_client.get(
|
||||
url_for("dashboard.encoded_unsubscribe", encoded_request=req_data),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert 200 == req.status_code
|
||||
assert not Alias.get(alias.id).enabled
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_request_disable_contact(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email=f"{random()}@sl.local",
|
||||
block_forward=False,
|
||||
commit=True,
|
||||
)
|
||||
req_data = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.DisableContact, contact.id
|
||||
)
|
||||
req = flask_client.get(
|
||||
url_for("dashboard.encoded_unsubscribe", encoded_request=req_data),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert 200 == req.status_code
|
||||
assert Contact.get(contact.id).block_forward
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_request_disable_newsletter(flask_client):
|
||||
user = login(flask_client)
|
||||
req_data = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.UnsubscribeNewsletter, user.id
|
||||
)
|
||||
req = flask_client.get(
|
||||
url_for("dashboard.encoded_unsubscribe", encoded_request=req_data),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert 200 == req.status_code
|
||||
assert not User.get(user.id).notification
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_request_original_unsub(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
original_recipient = f"{random()}@out.com"
|
||||
original_subject = f"Unsubsomehow{random()}"
|
||||
mail_sender.purge_stored_emails()
|
||||
req_data = UnsubscribeEncoder.encode_subject(
|
||||
UnsubscribeAction.OriginalUnsubscribeMailto,
|
||||
UnsubscribeOriginalData(alias.id, original_recipient, original_subject),
|
||||
)
|
||||
req = flask_client.get(
|
||||
url_for("dashboard.encoded_unsubscribe", encoded_request=req_data),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert 200 == req.status_code
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
mail_sent = mail_sender.get_stored_emails()[0]
|
||||
assert mail_sent.envelope_to == original_recipient
|
||||
name, address = parse_full_address(mail_sent.msg[headers.FROM])
|
||||
assert name == ""
|
||||
assert alias.email == address
|
||||
assert mail_sent.msg[headers.TO] == original_recipient
|
||||
assert mail_sent.msg[headers.SUBJECT] == original_subject
|
0
app/tests/jobs/__init__.py
Normal file
0
app/tests/jobs/__init__.py
Normal file
145
app/tests/jobs/test_export_user_data_job.py
Normal file
145
app/tests/jobs/test_export_user_data_job.py
Normal file
@ -0,0 +1,145 @@
|
||||
import zipfile
|
||||
from random import random
|
||||
|
||||
from app.db import Session
|
||||
from app.jobs.export_user_data_job import ExportUserDataJob
|
||||
from app.models import (
|
||||
Contact,
|
||||
Directory,
|
||||
DirectoryMailbox,
|
||||
RefusedEmail,
|
||||
CustomDomain,
|
||||
EmailLog,
|
||||
Alias,
|
||||
)
|
||||
from tests.utils import create_new_user, random_token
|
||||
|
||||
|
||||
def test_model_retrieval_and_serialization():
|
||||
user = create_new_user()
|
||||
job = ExportUserDataJob(user)
|
||||
ExportUserDataJob._model_to_dict(user)
|
||||
|
||||
# Aliases
|
||||
aliases = job._get_aliases()
|
||||
assert len(aliases) == 1
|
||||
ExportUserDataJob._model_to_dict(aliases[0])
|
||||
|
||||
# Mailboxes
|
||||
mailboxes = job._get_mailboxes()
|
||||
assert len(mailboxes) == 1
|
||||
ExportUserDataJob._model_to_dict(mailboxes[0])
|
||||
|
||||
# Contacts
|
||||
alias = aliases[0]
|
||||
contact = Contact.create(
|
||||
website_email=f"marketing-{random()}@example.com",
|
||||
reply_email=f"reply-{random()}@a.b",
|
||||
alias_id=alias.id,
|
||||
user_id=alias.user_id,
|
||||
commit=True,
|
||||
)
|
||||
contacts = job._get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contact.id == contacts[0].id
|
||||
ExportUserDataJob._model_to_dict(contacts[0])
|
||||
|
||||
# Directories
|
||||
dir_name = random_token()
|
||||
directory = Directory.create(name=dir_name, user_id=user.id, flush=True)
|
||||
DirectoryMailbox.create(
|
||||
directory_id=directory.id, mailbox_id=user.default_mailbox_id, flush=True
|
||||
)
|
||||
directories = job._get_directories()
|
||||
assert len(directories) == 1
|
||||
assert directory.id == directories[0].id
|
||||
ExportUserDataJob._model_to_dict(directories[0])
|
||||
|
||||
# CustomDomain
|
||||
custom_domain = CustomDomain.create(
|
||||
domain=f"{random()}.com", user_id=user.id, commit=True
|
||||
)
|
||||
domains = job._get_domains()
|
||||
assert len(domains) == 1
|
||||
assert custom_domain.id == domains[0].id
|
||||
ExportUserDataJob._model_to_dict(domains[0])
|
||||
|
||||
# RefusedEmails
|
||||
refused_email = RefusedEmail.create(
|
||||
path=None,
|
||||
full_report_path=f"some/path/{random()}",
|
||||
user_id=alias.user_id,
|
||||
commit=True,
|
||||
)
|
||||
refused_emails = job._get_refused_emails()
|
||||
assert len(refused_emails) == 1
|
||||
assert refused_email.id == refused_emails[0].id
|
||||
ExportUserDataJob._model_to_dict(refused_emails[0])
|
||||
|
||||
# EmailLog
|
||||
email_log = EmailLog.create(
|
||||
user_id=user.id,
|
||||
refused_email_id=refused_email.id,
|
||||
mailbox_id=alias.mailbox.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=alias.id,
|
||||
commit=True,
|
||||
)
|
||||
email_logs = job._get_email_logs()
|
||||
assert len(email_logs) == 1
|
||||
assert email_log.id == email_logs[0].id
|
||||
ExportUserDataJob._model_to_dict(email_logs[0])
|
||||
|
||||
# Get zip
|
||||
memfile = job._build_zip()
|
||||
files_in_zip = set()
|
||||
with zipfile.ZipFile(memfile, "r") as zf:
|
||||
for file_info in zf.infolist():
|
||||
files_in_zip.add(file_info.filename)
|
||||
assert file_info.file_size > 0
|
||||
expected_files_in_zip = set(
|
||||
(
|
||||
"user.json",
|
||||
"aliases.json",
|
||||
"mailboxes.json",
|
||||
"contacts.json",
|
||||
"directories.json",
|
||||
"domains.json",
|
||||
"email_logs.json",
|
||||
# "refused_emails.json",
|
||||
)
|
||||
)
|
||||
assert expected_files_in_zip == files_in_zip
|
||||
|
||||
|
||||
def test_model_retrieval_pagination():
|
||||
user = create_new_user()
|
||||
aliases = Session.query(Alias).filter(Alias.user_id == user.id).all()
|
||||
for _i in range(5):
|
||||
aliases.append(Alias.create_new_random(user))
|
||||
Session.commit()
|
||||
found_aliases = ExportUserDataJob(user)._get_paginated_model(Alias, 2)
|
||||
assert len(found_aliases) == len(aliases)
|
||||
|
||||
|
||||
def test_send_report():
|
||||
user = create_new_user()
|
||||
ExportUserDataJob(user).run()
|
||||
|
||||
|
||||
def test_store_and_retrieve():
|
||||
user = create_new_user()
|
||||
export_job = ExportUserDataJob(user)
|
||||
db_job = export_job.store_job_in_db()
|
||||
assert db_job is not None
|
||||
export_from_from_db = ExportUserDataJob.create_from_job(db_job)
|
||||
assert export_job._user.id == export_from_from_db._user.id
|
||||
|
||||
|
||||
def test_double_store_fails():
|
||||
user = create_new_user()
|
||||
export_job = ExportUserDataJob(user)
|
||||
db_job = export_job.store_job_in_db()
|
||||
assert db_job is not None
|
||||
retry = export_job.store_job_in_db()
|
||||
assert retry is None
|
71
app/tests/jobs/test_job_runner.py
Normal file
71
app/tests/jobs/test_job_runner.py
Normal file
@ -0,0 +1,71 @@
|
||||
from app import config
|
||||
from app.db import Session
|
||||
from job_runner import get_jobs_to_run
|
||||
from app.models import Job, JobState
|
||||
import arrow
|
||||
|
||||
|
||||
def test_get_jobs_to_run(flask_client):
|
||||
now = arrow.now()
|
||||
for job in Job.all():
|
||||
Job.delete(job.id)
|
||||
expected_jobs_to_run = [
|
||||
# Jobs in ready state
|
||||
Job.create(name="", payload=""),
|
||||
Job.create(name="", payload="", run_at=now),
|
||||
# Jobs in taken state
|
||||
Job.create(
|
||||
name="",
|
||||
payload="",
|
||||
state=JobState.taken.value,
|
||||
taken_at=now.shift(minutes=-(config.JOB_TAKEN_RETRY_WAIT_MINS + 10)),
|
||||
),
|
||||
Job.create(
|
||||
name="",
|
||||
payload="",
|
||||
state=JobState.taken.value,
|
||||
taken_at=now.shift(minutes=-(config.JOB_TAKEN_RETRY_WAIT_MINS + 10)),
|
||||
attempts=config.JOB_MAX_ATTEMPTS - 1,
|
||||
),
|
||||
Job.create(
|
||||
name="",
|
||||
payload="",
|
||||
state=JobState.taken.value,
|
||||
taken_at=now.shift(minutes=-(config.JOB_TAKEN_RETRY_WAIT_MINS + 10)),
|
||||
run_at=now,
|
||||
),
|
||||
]
|
||||
# Jobs not to run
|
||||
# Job to run in the future
|
||||
Job.create(name="", payload="", run_at=now.shift(hours=2))
|
||||
# Job in done state
|
||||
Job.create(name="", payload="", state=JobState.done.value)
|
||||
# Job taken but not enough time has passed
|
||||
Job.create(
|
||||
name="",
|
||||
payload="",
|
||||
state=JobState.taken.value,
|
||||
taken_at=now.shift(minutes=-(config.JOB_TAKEN_RETRY_WAIT_MINS - 10)),
|
||||
)
|
||||
# Job taken with enough time but out of run_at zone
|
||||
Job.create(
|
||||
name="",
|
||||
payload="",
|
||||
state=JobState.taken.value,
|
||||
taken_at=now.shift(minutes=-(config.JOB_TAKEN_RETRY_WAIT_MINS + 10)),
|
||||
run_at=now.shift(hours=3),
|
||||
)
|
||||
# Job out of attempts
|
||||
Job.create(
|
||||
name="",
|
||||
payload="",
|
||||
state=JobState.taken.value,
|
||||
taken_at=now.shift(minutes=-(config.JOB_TAKEN_RETRY_WAIT_MINS + 10)),
|
||||
attempts=config.JOB_MAX_ATTEMPTS + 1,
|
||||
),
|
||||
Session.commit()
|
||||
jobs = get_jobs_to_run()
|
||||
assert len(jobs) == len(expected_jobs_to_run)
|
||||
job_ids = [job.id for job in jobs]
|
||||
for job in expected_jobs_to_run:
|
||||
assert job.id in job_ids
|
14
app/tests/jobs/test_send_proton_welcome.py
Normal file
14
app/tests/jobs/test_send_proton_welcome.py
Normal file
@ -0,0 +1,14 @@
|
||||
from app.mail_sender import mail_sender
|
||||
from job_runner import welcome_proton
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_send_welcome_proton_email():
|
||||
user = create_new_user()
|
||||
welcome_proton(user)
|
||||
sent_mails = mail_sender.get_stored_emails()
|
||||
assert len(sent_mails) == 1
|
||||
sent_mail = sent_mails[0]
|
||||
comm_email, _, _ = user.get_communication_email()
|
||||
sent_mail.envelope_to = comm_email
|
0
app/tests/models/__init__.py
Normal file
0
app/tests/models/__init__.py
Normal file
25
app/tests/models/test_partner_api_token.py
Normal file
25
app/tests/models/test_partner_api_token.py
Normal file
@ -0,0 +1,25 @@
|
||||
from app.models import Partner, PartnerApiToken
|
||||
from app.utils import random_string
|
||||
|
||||
|
||||
def test_generate_partner_api_token(flask_client):
|
||||
partner = Partner.create(
|
||||
name=random_string(10),
|
||||
contact_email="{s}@{s}.com".format(s=random_string(10)),
|
||||
commit=True,
|
||||
)
|
||||
|
||||
partner_api_token, token = PartnerApiToken.generate(partner.id, None)
|
||||
|
||||
assert token is not None
|
||||
assert len(token) > 0
|
||||
|
||||
assert partner_api_token.partner_id == partner.id
|
||||
assert partner_api_token.expiration_time is None
|
||||
|
||||
hmaced = PartnerApiToken.hmac_token(token)
|
||||
assert hmaced == partner_api_token.token
|
||||
|
||||
retrieved_partner = Partner.find_by_token(token)
|
||||
assert retrieved_partner is not None
|
||||
assert retrieved_partner.id == partner.id
|
39
app/tests/models/test_partner_subscription.py
Normal file
39
app/tests/models/test_partner_subscription.py
Normal file
@ -0,0 +1,39 @@
|
||||
from arrow import Arrow
|
||||
from app.models import Partner, PartnerUser, PartnerSubscription
|
||||
from app.utils import random_string
|
||||
from tests.utils import create_new_user, random_email
|
||||
|
||||
|
||||
def test_generate_partner_subscription(flask_client):
|
||||
external_user_id = random_string()
|
||||
partner = Partner.create(
|
||||
name=random_string(10),
|
||||
contact_email=random_email(),
|
||||
commit=True,
|
||||
)
|
||||
user = create_new_user()
|
||||
partner_user = PartnerUser.create(
|
||||
user_id=user.id,
|
||||
partner_id=partner.id,
|
||||
partner_email=random_email(),
|
||||
external_user_id=external_user_id,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
subs = PartnerSubscription.create(
|
||||
partner_user_id=partner_user.id,
|
||||
end_at=Arrow.utcnow().shift(hours=1),
|
||||
commit=True,
|
||||
)
|
||||
|
||||
retrieved_subscription = PartnerSubscription.find_by_user_id(user.id)
|
||||
|
||||
assert retrieved_subscription is not None
|
||||
assert retrieved_subscription.id == subs.id
|
||||
|
||||
assert user.lifetime_or_active_subscription() is True
|
||||
|
||||
|
||||
def test_partner_subscription_for_not_partner_subscription_user(flask_client):
|
||||
unexistant_subscription = PartnerSubscription.find_by_user_id(999999)
|
||||
assert unexistant_subscription is None
|
31
app/tests/models/test_user.py
Normal file
31
app/tests/models/test_user.py
Normal file
@ -0,0 +1,31 @@
|
||||
from app import config
|
||||
from app.db import Session
|
||||
from app.models import User, Job
|
||||
from tests.utils import create_new_user, random_email
|
||||
|
||||
|
||||
def test_available_sl_domains(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
assert set(user.available_sl_domains()) == {"d1.test", "d2.test", "sl.local"}
|
||||
|
||||
|
||||
def test_create_from_partner(flask_client):
|
||||
user = User.create(email=random_email(), from_partner=True)
|
||||
assert User.FLAG_CREATED_FROM_PARTNER == (
|
||||
user.flags & User.FLAG_CREATED_FROM_PARTNER
|
||||
)
|
||||
assert user.notification is False
|
||||
assert user.trial_end is None
|
||||
job = Session.query(Job).order_by(Job.id.desc()).first()
|
||||
assert job is not None
|
||||
assert job.name == config.JOB_SEND_PROTON_WELCOME_1
|
||||
assert job.payload.get("user_id") == user.id
|
||||
|
||||
|
||||
def test_user_created_by_partner(flask_client):
|
||||
user_from_partner = User.create(email=random_email(), from_partner=True)
|
||||
assert user_from_partner.created_by_partner is True
|
||||
|
||||
regular_user = User.create(email=random_email())
|
||||
assert regular_user.created_by_partner is False
|
0
app/tests/oauth/__init__.py
Normal file
0
app/tests/oauth/__init__.py
Normal file
749
app/tests/oauth/test_authorize.py
Normal file
749
app/tests/oauth/test_authorize.py
Normal file
@ -0,0 +1,749 @@
|
||||
import base64
|
||||
import json
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from app.db import Session
|
||||
from app.jose_utils import verify_id_token, decode_id_token
|
||||
from app.models import Client, User, ClientUser, RedirectUri
|
||||
from app.oauth.views.authorize import (
|
||||
get_host_name_and_scheme,
|
||||
generate_access_token,
|
||||
construct_url,
|
||||
)
|
||||
from tests.utils import login, random_domain, random_string, random_email
|
||||
|
||||
|
||||
def generate_random_uri() -> str:
|
||||
return f"https://{random_domain()}/callback"
|
||||
|
||||
|
||||
def test_get_host_name_and_scheme():
|
||||
assert get_host_name_and_scheme("http://localhost:8000?a=b") == (
|
||||
"localhost",
|
||||
"http",
|
||||
)
|
||||
|
||||
assert get_host_name_and_scheme(
|
||||
"https://www.bubblecode.net/en/2016/01/22/understanding-oauth2/#Implicit_Grant"
|
||||
) == ("www.bubblecode.net", "https")
|
||||
|
||||
|
||||
def test_generate_access_token(flask_client):
|
||||
access_token = generate_access_token()
|
||||
assert len(access_token) == 40
|
||||
|
||||
|
||||
def test_construct_url():
|
||||
url = construct_url("http://ab.cd", {"x": "1 2"})
|
||||
assert url == "http://ab.cd?x=1%202"
|
||||
|
||||
|
||||
def test_authorize_page_non_login_user(flask_client):
|
||||
"""make sure to display login page for non-authenticated user"""
|
||||
user = User.create(random_email(), random_string())
|
||||
Session.commit()
|
||||
|
||||
client = Client.create_new(random_string(), user.id)
|
||||
Session.commit()
|
||||
|
||||
uri = generate_random_uri()
|
||||
RedirectUri.create(
|
||||
client_id=client.id,
|
||||
uri=uri,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
r = flask_client.get(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri=uri,
|
||||
response_type="code",
|
||||
)
|
||||
)
|
||||
|
||||
html = r.get_data(as_text=True)
|
||||
assert r.status_code == 200
|
||||
assert "Sign in to accept sharing data with" in html
|
||||
|
||||
|
||||
def test_authorize_page_login_user_non_supported_flow(flask_client):
|
||||
"""return 400 if the flow is not supported"""
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
Session.commit()
|
||||
|
||||
# Not provide any flow
|
||||
r = flask_client.get(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri="http://localhost",
|
||||
# not provide response_type param here
|
||||
)
|
||||
)
|
||||
|
||||
# Provide a not supported flow
|
||||
html = r.get_data(as_text=True)
|
||||
assert r.status_code == 400
|
||||
assert "SimpleLogin only support the following OIDC flows" in html
|
||||
|
||||
r = flask_client.get(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri="http://localhost",
|
||||
# SL does not support this flow combination
|
||||
response_type="code token id_token",
|
||||
)
|
||||
)
|
||||
|
||||
html = r.get_data(as_text=True)
|
||||
assert r.status_code == 400
|
||||
assert "SimpleLogin only support the following OIDC flows" in html
|
||||
|
||||
|
||||
def test_authorize_page_login_user(flask_client):
|
||||
"""make sure to display authorization page for authenticated user"""
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
|
||||
Session.commit()
|
||||
|
||||
uri = generate_random_uri()
|
||||
RedirectUri.create(
|
||||
client_id=client.id,
|
||||
uri=uri,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
r = flask_client.get(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri=uri,
|
||||
response_type="code",
|
||||
)
|
||||
)
|
||||
|
||||
html = r.get_data(as_text=True)
|
||||
assert r.status_code == 200
|
||||
assert f"{user.email} (Personal Email)" in html
|
||||
|
||||
|
||||
def test_authorize_code_flow_no_openid_scope(flask_client):
|
||||
"""make sure the authorize redirects user to correct page for the *Code Flow*
|
||||
and when the *openid* scope is not present
|
||||
, ie when response_type=code, openid not in scope
|
||||
"""
|
||||
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
Session.commit()
|
||||
domain = random_domain()
|
||||
uri = f"https://{domain}/callback"
|
||||
RedirectUri.create(
|
||||
client_id=client.id,
|
||||
uri=uri,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# user allows client on the authorization page
|
||||
r = flask_client.post(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri=uri,
|
||||
response_type="code",
|
||||
),
|
||||
data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
|
||||
# user will be redirected to client page, do not allow redirection here
|
||||
# to assert the redirect url
|
||||
# follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 302 # user gets redirected back to client page
|
||||
|
||||
# r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
|
||||
o = urlparse(r.location)
|
||||
assert o.netloc == domain
|
||||
assert not o.fragment
|
||||
|
||||
# parse the query, should return something like
|
||||
# {'state': ['teststate'], 'code': ['knuyjepwvg']}
|
||||
queries = parse_qs(o.query)
|
||||
assert len(queries) == 2
|
||||
|
||||
assert queries["state"] == ["teststate"]
|
||||
assert len(queries["code"]) == 1
|
||||
|
||||
# Exchange the code to get access_token
|
||||
basic_auth_headers = base64.b64encode(
|
||||
f"{client.oauth_client_id}:{client.oauth_client_secret}".encode()
|
||||
).decode("utf-8")
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("oauth.token"),
|
||||
headers={"Authorization": "Basic " + basic_auth_headers},
|
||||
data={"grant_type": "authorization_code", "code": queries["code"][0]},
|
||||
)
|
||||
|
||||
# r.json should have this format
|
||||
# {
|
||||
# 'access_token': 'avmhluhonsouhcwwailydwvhankspptgidoggcbu',
|
||||
# 'expires_in': 3600,
|
||||
# 'scope': '',
|
||||
# 'token_type': 'bearer',
|
||||
# 'user': {
|
||||
# 'avatar_url': None,
|
||||
# 'client': 'test client',
|
||||
# 'email': 'x@y.z',
|
||||
# 'email_verified': True,
|
||||
# 'id': 1,
|
||||
# 'name': 'AB CD'
|
||||
# }
|
||||
# }
|
||||
assert r.status_code == 200
|
||||
assert r.json["access_token"]
|
||||
assert r.json["expires_in"] == 3600
|
||||
assert not r.json["scope"]
|
||||
assert r.json["token_type"] == "Bearer"
|
||||
|
||||
client_user = ClientUser.get_by(client_id=client.id)
|
||||
|
||||
assert r.json["user"] == {
|
||||
"avatar_url": None,
|
||||
"client": "test client",
|
||||
"email": "x@y.z",
|
||||
"email_verified": True,
|
||||
"id": client_user.id,
|
||||
"name": "AB CD",
|
||||
"sub": str(client_user.id),
|
||||
}
|
||||
|
||||
|
||||
def test_authorize_code_flow_with_openid_scope(flask_client):
|
||||
"""make sure the authorize redirects user to correct page for the *Code Flow*
|
||||
and when the *openid* scope is present
|
||||
, ie when response_type=code, openid in scope
|
||||
|
||||
The authorize endpoint should stay the same: return the *code*.
|
||||
The token endpoint however should now return id_token in addition to the access_token
|
||||
"""
|
||||
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
|
||||
Session.commit()
|
||||
|
||||
domain = random_domain()
|
||||
uri = f"https://{domain}/callback"
|
||||
RedirectUri.create(
|
||||
client_id=client.id,
|
||||
uri=uri,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# user allows client on the authorization page
|
||||
r = flask_client.post(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri=uri,
|
||||
response_type="code",
|
||||
scope="openid", # openid is in scope
|
||||
),
|
||||
data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
|
||||
# user will be redirected to client page, do not allow redirection here
|
||||
# to assert the redirect url
|
||||
# follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 302 # user gets redirected back to client page
|
||||
|
||||
# r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
|
||||
o = urlparse(r.location)
|
||||
assert o.netloc == domain
|
||||
assert not o.fragment
|
||||
|
||||
# parse the query, should return something like
|
||||
# {'state': ['teststate'], 'code': ['knuyjepwvg'], 'scope': ["openid"]}
|
||||
queries = parse_qs(o.query)
|
||||
assert len(queries) == 3
|
||||
|
||||
assert queries["state"] == ["teststate"]
|
||||
assert len(queries["code"]) == 1
|
||||
|
||||
# Exchange the code to get access_token
|
||||
basic_auth_headers = base64.b64encode(
|
||||
f"{client.oauth_client_id}:{client.oauth_client_secret}".encode()
|
||||
).decode("utf-8")
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("oauth.token"),
|
||||
headers={"Authorization": "Basic " + basic_auth_headers},
|
||||
data={"grant_type": "authorization_code", "code": queries["code"][0]},
|
||||
)
|
||||
|
||||
# r.json should have this format
|
||||
# {
|
||||
# 'access_token': 'avmhluhonsouhcwwailydwvhankspptgidoggcbu',
|
||||
# 'expires_in': 3600,
|
||||
# 'scope': '',
|
||||
# 'token_type': 'bearer',
|
||||
# 'user': {
|
||||
# 'avatar_url': None,
|
||||
# 'client': 'test client',
|
||||
# 'email': 'x@y.z',
|
||||
# 'email_verified': True,
|
||||
# 'id': 1,
|
||||
# 'name': 'AB CD'
|
||||
# }
|
||||
# }
|
||||
assert r.status_code == 200
|
||||
assert r.json["access_token"]
|
||||
assert r.json["expires_in"] == 3600
|
||||
assert r.json["scope"] == "openid"
|
||||
assert r.json["token_type"] == "Bearer"
|
||||
|
||||
client_user = ClientUser.get_by(client_id=client.id)
|
||||
|
||||
assert r.json["user"] == {
|
||||
"avatar_url": None,
|
||||
"client": "test client",
|
||||
"email": "x@y.z",
|
||||
"email_verified": True,
|
||||
"id": client_user.id,
|
||||
"name": "AB CD",
|
||||
"sub": str(client_user.id),
|
||||
}
|
||||
|
||||
# id_token must be returned
|
||||
assert r.json["id_token"]
|
||||
|
||||
# id_token must be a valid, correctly signed JWT
|
||||
assert verify_id_token(r.json["id_token"])
|
||||
|
||||
|
||||
def test_authorize_token_flow(flask_client):
|
||||
"""make sure the authorize redirects user to correct page for the *Token Flow*
|
||||
, ie when response_type=token
|
||||
The /authorize endpoint should return an access_token
|
||||
"""
|
||||
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
|
||||
Session.commit()
|
||||
domain = random_domain()
|
||||
uri = f"https://{domain}/callback"
|
||||
RedirectUri.create(
|
||||
client_id=client.id,
|
||||
uri=uri,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# user allows client on the authorization page
|
||||
r = flask_client.post(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri=uri,
|
||||
response_type="token", # token flow
|
||||
),
|
||||
data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
|
||||
# user will be redirected to client page, do not allow redirection here
|
||||
# to assert the redirect url
|
||||
# follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 302 # user gets redirected back to client page
|
||||
|
||||
# r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
|
||||
o = urlparse(r.location)
|
||||
assert o.netloc == domain
|
||||
|
||||
# in token flow, access_token is in fragment and not query
|
||||
assert o.fragment
|
||||
assert not o.query
|
||||
|
||||
# parse the fragment, should return something like
|
||||
# {'state': ['teststate'], 'access_token': ['knuyjepwvg']}
|
||||
queries = parse_qs(o.fragment)
|
||||
assert len(queries) == 2
|
||||
|
||||
assert queries["state"] == ["teststate"]
|
||||
|
||||
# access_token must be returned
|
||||
assert len(queries["access_token"]) == 1
|
||||
|
||||
|
||||
def test_authorize_id_token_flow(flask_client):
|
||||
"""make sure the authorize redirects user to correct page for the *ID-Token Flow*
|
||||
, ie when response_type=id_token
|
||||
The /authorize endpoint should return an id_token
|
||||
"""
|
||||
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
|
||||
Session.commit()
|
||||
domain = random_domain()
|
||||
uri = f"https://{domain}/callback"
|
||||
RedirectUri.create(
|
||||
client_id=client.id,
|
||||
uri=uri,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# user allows client on the authorization page
|
||||
r = flask_client.post(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri=uri,
|
||||
response_type="id_token", # id_token flow
|
||||
),
|
||||
data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
|
||||
# user will be redirected to client page, do not allow redirection here
|
||||
# to assert the redirect url
|
||||
# follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 302 # user gets redirected back to client page
|
||||
|
||||
# r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
|
||||
o = urlparse(r.location)
|
||||
assert o.netloc == domain
|
||||
assert not o.fragment
|
||||
assert o.query
|
||||
|
||||
# parse the fragment, should return something like
|
||||
# {'state': ['teststate'], 'id_token': ['knuyjepwvg']}
|
||||
queries = parse_qs(o.query)
|
||||
assert len(queries) == 2
|
||||
|
||||
assert queries["state"] == ["teststate"]
|
||||
|
||||
# access_token must be returned
|
||||
assert len(queries["id_token"]) == 1
|
||||
|
||||
# id_token must be a valid, correctly signed JWT
|
||||
assert verify_id_token(queries["id_token"][0])
|
||||
|
||||
|
||||
def test_authorize_token_id_token_flow(flask_client):
|
||||
"""make sure the authorize redirects user to correct page for the *ID-Token Token Flow*
|
||||
, ie when response_type=id_token,token
|
||||
The /authorize endpoint should return an id_token and access_token
|
||||
id_token, once decoded, should contain *at_hash* in payload
|
||||
"""
|
||||
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
|
||||
Session.commit()
|
||||
domain = random_domain()
|
||||
uri = f"https://{domain}/callback"
|
||||
RedirectUri.create(
|
||||
client_id=client.id,
|
||||
uri=uri,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# user allows client on the authorization page
|
||||
r = flask_client.post(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri=uri,
|
||||
response_type="id_token token", # id_token,token flow
|
||||
),
|
||||
data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
|
||||
# user will be redirected to client page, do not allow redirection here
|
||||
# to assert the redirect url
|
||||
# follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 302 # user gets redirected back to client page
|
||||
|
||||
# r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
|
||||
o = urlparse(r.location)
|
||||
assert o.netloc == domain
|
||||
assert o.fragment
|
||||
assert not o.query
|
||||
|
||||
# parse the fragment, should return something like
|
||||
# {'state': ['teststate'], 'id_token': ['knuyjepwvg']}
|
||||
queries = parse_qs(o.fragment)
|
||||
assert len(queries) == 3
|
||||
|
||||
assert queries["state"] == ["teststate"]
|
||||
|
||||
# access_token must be returned
|
||||
assert len(queries["id_token"]) == 1
|
||||
assert len(queries["access_token"]) == 1
|
||||
|
||||
# id_token must be a valid, correctly signed JWT
|
||||
id_token = queries["id_token"][0]
|
||||
assert verify_id_token(id_token)
|
||||
|
||||
# make sure jwt has all the necessary fields
|
||||
jwt = decode_id_token(id_token)
|
||||
|
||||
# payload should have this format
|
||||
# {
|
||||
# 'at_hash': 'jLDmoGpuOIHwxeyFEe9SKw',
|
||||
# 'aud': 'testclient-sywcpwsyua',
|
||||
# 'auth_time': 1565450736,
|
||||
# 'avatar_url': None,
|
||||
# 'client': 'test client',
|
||||
# 'email': 'x@y.z',
|
||||
# 'email_verified': True,
|
||||
# 'exp': 1565454336,
|
||||
# 'iat': 1565450736,
|
||||
# 'id': 1,
|
||||
# 'iss': 'http://localhost',
|
||||
# 'name': 'AB CD',
|
||||
# 'sub': '1'
|
||||
# }
|
||||
payload = json.loads(jwt.claims)
|
||||
|
||||
# at_hash MUST be present when the flow is id_token,token
|
||||
assert "at_hash" in payload
|
||||
|
||||
assert "aud" in payload
|
||||
assert "auth_time" in payload
|
||||
assert "avatar_url" in payload
|
||||
assert "client" in payload
|
||||
assert "email" in payload
|
||||
assert "email_verified" in payload
|
||||
assert "exp" in payload
|
||||
assert "iat" in payload
|
||||
assert "id" in payload
|
||||
assert "iss" in payload
|
||||
assert "name" in payload
|
||||
assert "sub" in payload
|
||||
|
||||
|
||||
def test_authorize_code_id_token_flow(flask_client):
|
||||
"""make sure the authorize redirects user to correct page for the *ID-Token Code Flow*
|
||||
, ie when response_type=id_token,code
|
||||
The /authorize endpoint should return an id_token, code and id_token must contain *c_hash*
|
||||
|
||||
The /token endpoint must return a access_token and an id_token
|
||||
|
||||
"""
|
||||
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
|
||||
Session.commit()
|
||||
domain = random_domain()
|
||||
uri = f"https://{domain}/callback"
|
||||
RedirectUri.create(
|
||||
client_id=client.id,
|
||||
uri=uri,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# user allows client on the authorization page
|
||||
r = flask_client.post(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri=uri,
|
||||
response_type="id_token code", # id_token,code flow
|
||||
),
|
||||
data={"button": "allow", "suggested-email": "x@y.z", "suggested-name": "AB CD"},
|
||||
# user will be redirected to client page, do not allow redirection here
|
||||
# to assert the redirect url
|
||||
# follow_redirects=True,
|
||||
)
|
||||
|
||||
assert r.status_code == 302 # user gets redirected back to client page
|
||||
|
||||
# r.location will have this form http://localhost?state=teststate&code=knuyjepwvg
|
||||
o = urlparse(r.location)
|
||||
assert o.netloc == domain
|
||||
assert not o.fragment
|
||||
assert o.query
|
||||
|
||||
# parse the query, should return something like
|
||||
# {'state': ['teststate'], 'id_token': ['knuyjepwvg'], 'code': ['longstring']}
|
||||
queries = parse_qs(o.query)
|
||||
assert len(queries) == 3
|
||||
|
||||
assert queries["state"] == ["teststate"]
|
||||
|
||||
assert len(queries["id_token"]) == 1
|
||||
assert len(queries["code"]) == 1
|
||||
|
||||
# id_token must be a valid, correctly signed JWT
|
||||
id_token = queries["id_token"][0]
|
||||
assert verify_id_token(id_token)
|
||||
|
||||
# make sure jwt has all the necessary fields
|
||||
jwt = decode_id_token(id_token)
|
||||
|
||||
# payload should have this format
|
||||
# {
|
||||
# 'at_hash': 'jLDmoGpuOIHwxeyFEe9SKw',
|
||||
# 'aud': 'testclient-sywcpwsyua',
|
||||
# 'auth_time': 1565450736,
|
||||
# 'avatar_url': None,
|
||||
# 'client': 'test client',
|
||||
# 'email': 'x@y.z',
|
||||
# 'email_verified': True,
|
||||
# 'exp': 1565454336,
|
||||
# 'iat': 1565450736,
|
||||
# 'id': 1,
|
||||
# 'iss': 'http://localhost',
|
||||
# 'name': 'AB CD',
|
||||
# 'sub': '1'
|
||||
# }
|
||||
payload = json.loads(jwt.claims)
|
||||
|
||||
# at_hash MUST be present when the flow is id_token,token
|
||||
assert "c_hash" in payload
|
||||
|
||||
assert "aud" in payload
|
||||
assert "auth_time" in payload
|
||||
assert "avatar_url" in payload
|
||||
assert "client" in payload
|
||||
assert "email" in payload
|
||||
assert "email_verified" in payload
|
||||
assert "exp" in payload
|
||||
assert "iat" in payload
|
||||
assert "id" in payload
|
||||
assert "iss" in payload
|
||||
assert "name" in payload
|
||||
assert "sub" in payload
|
||||
|
||||
# <<< Exchange the code to get access_token >>>
|
||||
basic_auth_headers = base64.b64encode(
|
||||
f"{client.oauth_client_id}:{client.oauth_client_secret}".encode()
|
||||
).decode("utf-8")
|
||||
|
||||
r = flask_client.post(
|
||||
url_for("oauth.token"),
|
||||
headers={"Authorization": "Basic " + basic_auth_headers},
|
||||
data={"grant_type": "authorization_code", "code": queries["code"][0]},
|
||||
)
|
||||
|
||||
# r.json should have this format
|
||||
# {
|
||||
# 'access_token': 'avmhluhonsouhcwwailydwvhankspptgidoggcbu',
|
||||
# 'id_token': 'ab.cd.xy',
|
||||
# 'expires_in': 3600,
|
||||
# 'scope': '',
|
||||
# 'token_type': 'bearer',
|
||||
# 'user': {
|
||||
# 'avatar_url': None,
|
||||
# 'client': 'test client',
|
||||
# 'email': 'x@y.z',
|
||||
# 'email_verified': True,
|
||||
# 'id': 1,
|
||||
# 'name': 'AB CD'
|
||||
# }
|
||||
# }
|
||||
assert r.status_code == 200
|
||||
assert r.json["access_token"]
|
||||
assert r.json["expires_in"] == 3600
|
||||
assert not r.json["scope"]
|
||||
assert r.json["token_type"] == "Bearer"
|
||||
|
||||
client_user = ClientUser.get_by(client_id=client.id)
|
||||
|
||||
assert r.json["user"] == {
|
||||
"avatar_url": None,
|
||||
"client": "test client",
|
||||
"email": "x@y.z",
|
||||
"email_verified": True,
|
||||
"id": client_user.id,
|
||||
"name": "AB CD",
|
||||
"sub": str(client_user.id),
|
||||
}
|
||||
|
||||
# id_token must be returned
|
||||
assert r.json["id_token"]
|
||||
|
||||
# id_token must be a valid, correctly signed JWT
|
||||
assert verify_id_token(r.json["id_token"])
|
||||
|
||||
|
||||
def test_authorize_page_invalid_client_id(flask_client):
|
||||
"""make sure to redirect user to redirect_url?error=invalid_client_id"""
|
||||
user = login(flask_client)
|
||||
Client.create_new("test client", user.id)
|
||||
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id="invalid_client_id",
|
||||
state="teststate",
|
||||
redirect_uri="http://localhost",
|
||||
response_type="code",
|
||||
)
|
||||
)
|
||||
|
||||
assert r.status_code == 302
|
||||
assert r.location == url_for("auth.login")
|
||||
|
||||
|
||||
def test_authorize_page_http_not_allowed(flask_client):
|
||||
"""make sure to redirect user to redirect_url?error=http_not_allowed"""
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
client.approved = True
|
||||
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri="http://mywebsite.com",
|
||||
response_type="code",
|
||||
)
|
||||
)
|
||||
|
||||
assert r.status_code == 302
|
||||
assert r.location == url_for("dashboard.index")
|
||||
|
||||
|
||||
def test_authorize_page_unknown_redirect_uri(flask_client):
|
||||
"""make sure to redirect user to redirect_url?error=unknown_redirect_uri"""
|
||||
user = login(flask_client)
|
||||
client = Client.create_new("test client", user.id)
|
||||
client.approved = True
|
||||
|
||||
Session.commit()
|
||||
|
||||
r = flask_client.get(
|
||||
url_for(
|
||||
"oauth.authorize",
|
||||
client_id=client.oauth_client_id,
|
||||
state="teststate",
|
||||
redirect_uri="https://unknown.com",
|
||||
response_type="code",
|
||||
)
|
||||
)
|
||||
|
||||
assert r.status_code == 302
|
||||
assert r.location == url_for("dashboard.index")
|
0
app/tests/proton/__init__.py
Normal file
0
app/tests/proton/__init__.py
Normal file
103
app/tests/proton/test_proton_callback_handler.py
Normal file
103
app/tests/proton/test_proton_callback_handler.py
Normal file
@ -0,0 +1,103 @@
|
||||
from arrow import Arrow
|
||||
from app.account_linking import (
|
||||
SLPlan,
|
||||
SLPlanType,
|
||||
)
|
||||
from app.proton.proton_client import ProtonClient, UserInformation
|
||||
from app.proton.proton_callback_handler import (
|
||||
ProtonCallbackHandler,
|
||||
generate_account_not_allowed_to_log_in,
|
||||
)
|
||||
from app.models import User, PartnerUser
|
||||
from app.proton.utils import get_proton_partner
|
||||
from app.utils import random_string
|
||||
from typing import Optional
|
||||
from tests.utils import random_email
|
||||
|
||||
|
||||
class MockProtonClient(ProtonClient):
|
||||
def __init__(self, user: Optional[UserInformation]):
|
||||
self.user = user
|
||||
|
||||
def get_user(self) -> Optional[UserInformation]:
|
||||
return self.user
|
||||
|
||||
|
||||
def test_proton_callback_handler_unexistant_sl_user():
|
||||
email = random_email()
|
||||
name = random_string()
|
||||
external_id = random_string()
|
||||
handler = ProtonCallbackHandler(
|
||||
MockProtonClient(
|
||||
user=UserInformation(
|
||||
email=email,
|
||||
name=name,
|
||||
id=external_id,
|
||||
plan=SLPlan(
|
||||
type=SLPlanType.Premium, expiration=Arrow.utcnow().shift(hours=2)
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
res = handler.handle_login(get_proton_partner())
|
||||
|
||||
assert res.user is not None
|
||||
assert res.user.email == email
|
||||
assert res.user.name == name
|
||||
# Ensure the user is not marked as created from partner
|
||||
assert User.FLAG_CREATED_FROM_PARTNER != (
|
||||
res.user.flags & User.FLAG_CREATED_FROM_PARTNER
|
||||
)
|
||||
assert res.user.notification is True
|
||||
assert res.user.trial_end is not None
|
||||
|
||||
partner_user = PartnerUser.get_by(
|
||||
partner_id=get_proton_partner().id, user_id=res.user.id
|
||||
)
|
||||
assert partner_user is not None
|
||||
assert partner_user.external_user_id == external_id
|
||||
|
||||
|
||||
def test_proton_callback_handler_existant_sl_user():
|
||||
email = random_email()
|
||||
sl_user = User.create(email, commit=True)
|
||||
|
||||
external_id = random_string()
|
||||
user = UserInformation(
|
||||
email=email,
|
||||
name=random_string(),
|
||||
id=external_id,
|
||||
plan=SLPlan(type=SLPlanType.Premium, expiration=Arrow.utcnow().shift(hours=2)),
|
||||
)
|
||||
handler = ProtonCallbackHandler(MockProtonClient(user=user))
|
||||
res = handler.handle_login(get_proton_partner())
|
||||
|
||||
assert res.user is not None
|
||||
assert res.user.id == sl_user.id
|
||||
# Ensure the user is not marked as created from partner
|
||||
assert User.FLAG_CREATED_FROM_PARTNER != (
|
||||
res.user.flags & User.FLAG_CREATED_FROM_PARTNER
|
||||
)
|
||||
assert res.user.notification is True
|
||||
assert res.user.trial_end is not None
|
||||
|
||||
sa = PartnerUser.get_by(user_id=sl_user.id, partner_id=get_proton_partner().id)
|
||||
assert sa is not None
|
||||
assert sa.partner_email == user.email
|
||||
|
||||
|
||||
def test_proton_callback_handler_none_user_login():
|
||||
handler = ProtonCallbackHandler(MockProtonClient(user=None))
|
||||
res = handler.handle_login(get_proton_partner())
|
||||
|
||||
expected = generate_account_not_allowed_to_log_in()
|
||||
assert res == expected
|
||||
|
||||
|
||||
def test_proton_callback_handler_none_user_link():
|
||||
sl_user = User.create(random_email(), commit=True)
|
||||
handler = ProtonCallbackHandler(MockProtonClient(user=None))
|
||||
res = handler.handle_link(sl_user, get_proton_partner())
|
||||
|
||||
expected = generate_account_not_allowed_to_log_in()
|
||||
assert res == expected
|
21
app/tests/proton/test_proton_client.py
Normal file
21
app/tests/proton/test_proton_client.py
Normal file
@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from app.proton import proton_client
|
||||
|
||||
|
||||
def test_convert_access_token_valid():
|
||||
res = proton_client.convert_access_token("pt-abc-123")
|
||||
assert res.session_id == "abc"
|
||||
assert res.access_token == "123"
|
||||
|
||||
|
||||
def test_convert_access_token_not_containing_pt():
|
||||
with pytest.raises(Exception):
|
||||
proton_client.convert_access_token("pb-abc-123")
|
||||
|
||||
|
||||
def test_convert_access_token_not_containing_invalid_length():
|
||||
cases = ["pt-abc-too-long", "pt-short"]
|
||||
for case in cases:
|
||||
with pytest.raises(Exception):
|
||||
proton_client.convert_access_token(case)
|
71
app/tests/test.env
Normal file
71
app/tests/test.env
Normal file
@ -0,0 +1,71 @@
|
||||
# Server url
|
||||
URL=http://localhost
|
||||
LOCAL_FILE_UPLOAD=1
|
||||
|
||||
# Email related settings
|
||||
# Only print email content, not sending it
|
||||
NOT_SEND_EMAIL=true
|
||||
EMAIL_DOMAIN=sl.local
|
||||
OTHER_ALIAS_DOMAINS=["d1.test", "d2.test", "sl.local"]
|
||||
SUPPORT_EMAIL=support@sl.local
|
||||
ADMIN_EMAIL=to_fill
|
||||
# Max number emails user can generate for free plan
|
||||
MAX_NB_EMAIL_FREE_PLAN=3
|
||||
EMAIL_SERVERS_WITH_PRIORITY=[(10, "email.hostname.")]
|
||||
DKIM_PRIVATE_KEY_PATH=local_data/dkim.key
|
||||
|
||||
DB_URI=postgresql://test:test@localhost:15432/test
|
||||
|
||||
# Flask
|
||||
FLASK_SECRET=secret
|
||||
|
||||
# AWS
|
||||
BUCKET=to_fill
|
||||
AWS_ACCESS_KEY_ID=to_fill
|
||||
AWS_SECRET_ACCESS_KEY=to_fill
|
||||
|
||||
# Paddle
|
||||
PADDLE_VENDOR_ID=1
|
||||
PADDLE_MONTHLY_PRODUCT_ID=2
|
||||
PADDLE_YEARLY_PRODUCT_ID=3
|
||||
PADDLE_PUBLIC_KEY_PATH=local_data/paddle.key.pub
|
||||
|
||||
# OpenId key
|
||||
OPENID_PRIVATE_KEY_PATH=local_data/jwtRS256.key
|
||||
OPENID_PUBLIC_KEY_PATH=local_data/jwtRS256.key.pub
|
||||
|
||||
# Words to generate random email alias
|
||||
WORDS_FILE_PATH=local_data/test_words.txt
|
||||
|
||||
# Github
|
||||
GITHUB_CLIENT_ID=to_fill
|
||||
GITHUB_CLIENT_SECRET=to_fill
|
||||
|
||||
# Google
|
||||
GOOGLE_CLIENT_ID=to_fill
|
||||
GOOGLE_CLIENT_SECRET=to_fill
|
||||
|
||||
# Facebook
|
||||
FACEBOOK_CLIENT_ID=to_fill
|
||||
FACEBOOK_CLIENT_SECRET=to_fill
|
||||
|
||||
PGP_SENDER_PRIVATE_KEY_PATH=local_data/private-pgp.asc
|
||||
|
||||
ALIAS_AUTOMATIC_DISABLE=true
|
||||
ALLOWED_REDIRECT_DOMAINS=["test.simplelogin.local"]
|
||||
|
||||
|
||||
DMARC_CHECK_ENABLED=true
|
||||
|
||||
PROTON_CLIENT_ID=to_fill
|
||||
PROTON_CLIENT_SECRET=to_fill
|
||||
PROTON_BASE_URL=https://localhost/api
|
||||
|
||||
POSTMASTER=postmaster@test.domain
|
||||
|
||||
RECOVERY_CODE_HMAC_SECRET=1234567890123456789
|
||||
ENABLE_ALL_REVERSE_ALIAS_REPLACEMENT=true
|
||||
MAX_NB_REVERSE_ALIAS_REPLACEMENT=200
|
||||
|
||||
MEM_STORE_URI=redis://localhost
|
||||
|
379
app/tests/test_account_linking.py
Normal file
379
app/tests/test_account_linking.py
Normal file
@ -0,0 +1,379 @@
|
||||
import pytest
|
||||
from arrow import Arrow
|
||||
|
||||
from app.account_linking import (
|
||||
process_link_case,
|
||||
process_login_case,
|
||||
get_login_strategy,
|
||||
ensure_partner_user_exists_for_user,
|
||||
NewUserStrategy,
|
||||
ExistingUnlinkedUserStrategy,
|
||||
LinkedWithAnotherPartnerUserStrategy,
|
||||
SLPlan,
|
||||
SLPlanType,
|
||||
PartnerLinkRequest,
|
||||
ClientMergeStrategy,
|
||||
)
|
||||
from app.db import Session
|
||||
from app.errors import AccountAlreadyLinkedToAnotherPartnerException
|
||||
from app.models import Partner, PartnerUser, User
|
||||
from app.proton.utils import get_proton_partner
|
||||
from app.utils import random_string
|
||||
from tests.utils import random_email
|
||||
|
||||
|
||||
def random_link_request(
|
||||
external_user_id: str = None,
|
||||
name: str = None,
|
||||
email: str = None,
|
||||
plan: SLPlan = None,
|
||||
from_partner: bool = False,
|
||||
) -> PartnerLinkRequest:
|
||||
external_user_id = (
|
||||
external_user_id if external_user_id is not None else random_string()
|
||||
)
|
||||
name = name if name is not None else random_string()
|
||||
email = email if email is not None else random_email()
|
||||
plan = plan if plan is not None else SLPlanType.Free
|
||||
return PartnerLinkRequest(
|
||||
name=name,
|
||||
email=email,
|
||||
external_user_id=external_user_id,
|
||||
plan=SLPlan(type=plan, expiration=Arrow.utcnow().shift(hours=2)),
|
||||
from_partner=from_partner,
|
||||
)
|
||||
|
||||
|
||||
def create_user(email: str = None) -> User:
|
||||
email = email if email is not None else random_email()
|
||||
user = User.create(email=email)
|
||||
Session.commit()
|
||||
return user
|
||||
|
||||
|
||||
def create_user_for_partner(external_user_id: str, email: str = None) -> User:
|
||||
email = email if email is not None else random_email()
|
||||
user = User.create(email=email)
|
||||
|
||||
PartnerUser.create(
|
||||
user_id=user.id,
|
||||
partner_id=get_proton_partner().id,
|
||||
partner_email=email,
|
||||
external_user_id=external_user_id,
|
||||
)
|
||||
Session.commit()
|
||||
return user
|
||||
|
||||
|
||||
def test_get_strategy_unexistant_sl_user():
|
||||
strategy = get_login_strategy(
|
||||
link_request=random_link_request(),
|
||||
user=None,
|
||||
partner=get_proton_partner(),
|
||||
)
|
||||
assert isinstance(strategy, NewUserStrategy)
|
||||
|
||||
|
||||
def test_login_case_from_partner():
|
||||
partner = get_proton_partner()
|
||||
res = process_login_case(
|
||||
random_link_request(
|
||||
external_user_id=random_string(),
|
||||
from_partner=True,
|
||||
),
|
||||
partner,
|
||||
)
|
||||
|
||||
assert res.strategy == NewUserStrategy.__name__
|
||||
assert res.user is not None
|
||||
assert User.FLAG_CREATED_FROM_PARTNER == (
|
||||
res.user.flags & User.FLAG_CREATED_FROM_PARTNER
|
||||
)
|
||||
assert res.user.activated is True
|
||||
|
||||
|
||||
def test_login_case_from_partner_with_uppercase_email():
|
||||
partner = get_proton_partner()
|
||||
link_request = random_link_request(
|
||||
external_user_id=random_string(),
|
||||
from_partner=True,
|
||||
)
|
||||
link_request.email = link_request.email.upper()
|
||||
res = process_login_case(link_request, partner)
|
||||
|
||||
assert res.strategy == NewUserStrategy.__name__
|
||||
assert res.user is not None
|
||||
assert res.user.email == link_request.email.lower()
|
||||
assert User.FLAG_CREATED_FROM_PARTNER == (
|
||||
res.user.flags & User.FLAG_CREATED_FROM_PARTNER
|
||||
)
|
||||
assert res.user.activated is True
|
||||
|
||||
|
||||
def test_login_case_from_web():
|
||||
partner = get_proton_partner()
|
||||
res = process_login_case(
|
||||
random_link_request(
|
||||
external_user_id=random_string(),
|
||||
from_partner=False,
|
||||
),
|
||||
partner,
|
||||
)
|
||||
|
||||
assert res.strategy == NewUserStrategy.__name__
|
||||
assert res.user is not None
|
||||
assert 0 == (res.user.flags & User.FLAG_CREATED_FROM_PARTNER)
|
||||
assert res.user.activated is True
|
||||
|
||||
|
||||
def test_get_strategy_existing_sl_user():
|
||||
email = random_email()
|
||||
user = User.create(email, commit=True)
|
||||
strategy = get_login_strategy(
|
||||
link_request=random_link_request(email=email),
|
||||
user=user,
|
||||
partner=get_proton_partner(),
|
||||
)
|
||||
assert isinstance(strategy, ExistingUnlinkedUserStrategy)
|
||||
|
||||
|
||||
def test_get_strategy_existing_sl_user_with_uppercase_email():
|
||||
email = random_email()
|
||||
user = User.create(email, commit=True)
|
||||
strategy = get_login_strategy(
|
||||
link_request=random_link_request(email=email.upper()),
|
||||
user=user,
|
||||
partner=get_proton_partner(),
|
||||
)
|
||||
assert isinstance(strategy, ExistingUnlinkedUserStrategy)
|
||||
|
||||
|
||||
def test_get_strategy_existing_sl_user_linked_with_different_proton_account():
|
||||
# In this scenario we have
|
||||
# - PartnerUser1 (ID1, email1@proton)
|
||||
# - PartnerUser2 (ID2, email2@proton)
|
||||
# - SimpleLoginUser1 registered with email1@proton, but linked to account ID2
|
||||
# We will try to log in with email1@proton
|
||||
email1 = random_email()
|
||||
email2 = random_email()
|
||||
partner_user_id_1 = random_string()
|
||||
partner_user_id_2 = random_string()
|
||||
|
||||
link_request_1 = random_link_request(
|
||||
external_user_id=partner_user_id_1, email=email1
|
||||
)
|
||||
link_request_2 = random_link_request(
|
||||
external_user_id=partner_user_id_2, email=email2
|
||||
)
|
||||
|
||||
user = create_user_for_partner(
|
||||
link_request_2.external_user_id, email=link_request_1.email
|
||||
)
|
||||
strategy = get_login_strategy(
|
||||
link_request=link_request_1,
|
||||
user=user,
|
||||
partner=get_proton_partner(),
|
||||
)
|
||||
assert isinstance(strategy, LinkedWithAnotherPartnerUserStrategy)
|
||||
|
||||
|
||||
##
|
||||
# LINK
|
||||
|
||||
|
||||
def test_link_account_with_proton_account_same_address(flask_client):
|
||||
# This is the most basic scenario
|
||||
# In this scenario we have:
|
||||
# - PartnerUser (email1@partner)
|
||||
# - SimpleLoginUser registered with email1@proton
|
||||
# We will try to link both accounts
|
||||
|
||||
email = random_email()
|
||||
partner_user_id = random_string()
|
||||
link_request = random_link_request(external_user_id=partner_user_id, email=email)
|
||||
user = create_user(email)
|
||||
|
||||
res = process_link_case(link_request, user, get_proton_partner())
|
||||
assert res is not None
|
||||
assert res.user is not None
|
||||
assert res.user.id == user.id
|
||||
assert res.user.email == email
|
||||
assert res.strategy == "Link"
|
||||
|
||||
partner_user = PartnerUser.get_by(
|
||||
partner_id=get_proton_partner().id, user_id=user.id
|
||||
)
|
||||
assert partner_user.partner_id == get_proton_partner().id
|
||||
assert partner_user.external_user_id == partner_user_id
|
||||
|
||||
|
||||
def test_link_account_with_proton_account_different_address(flask_client):
|
||||
# In this scenario we have:
|
||||
# - ProtonUser (foo@proton)
|
||||
# - SimpleLoginUser (bar@somethingelse)
|
||||
# We will try to link both accounts
|
||||
partner_user_id = random_string()
|
||||
link_request = random_link_request(
|
||||
external_user_id=partner_user_id, email=random_email()
|
||||
)
|
||||
user = create_user()
|
||||
|
||||
res = process_link_case(link_request, user, get_proton_partner())
|
||||
assert res.user.id == user.id
|
||||
assert res.user.email == user.email
|
||||
assert res.strategy == "Link"
|
||||
|
||||
partner_user = PartnerUser.get_by(
|
||||
partner_id=get_proton_partner().id, user_id=user.id
|
||||
)
|
||||
assert partner_user.partner_id == get_proton_partner().id
|
||||
assert partner_user.external_user_id == partner_user_id
|
||||
|
||||
|
||||
def test_link_account_with_proton_account_same_address_but_linked_to_other_user(
|
||||
flask_client,
|
||||
):
|
||||
# In this scenario we have:
|
||||
# - PartnerUser (foo@partner)
|
||||
# - SimpleLoginUser1 (foo@partner)
|
||||
# - SimpleLoginUser2 (other@somethingelse) linked with foo@partner
|
||||
# We will unlink SimpleLoginUser2 and link SimpleLoginUser1 with foo@partner
|
||||
partner_user_id = random_string()
|
||||
partner_email = random_email()
|
||||
link_request = random_link_request(
|
||||
external_user_id=partner_user_id, email=partner_email
|
||||
)
|
||||
sl_user_1 = create_user(partner_email)
|
||||
sl_user_2 = create_user_for_partner(
|
||||
partner_user_id, email=random_email()
|
||||
) # User already linked with the proton account
|
||||
|
||||
res = process_link_case(link_request, sl_user_1, get_proton_partner())
|
||||
assert res.user.id == sl_user_1.id
|
||||
assert res.user.email == partner_email
|
||||
assert res.strategy == "Link"
|
||||
|
||||
partner_user = PartnerUser.get_by(
|
||||
partner_id=get_proton_partner().id, user_id=sl_user_1.id
|
||||
)
|
||||
assert partner_user.partner_id == get_proton_partner().id
|
||||
assert partner_user.external_user_id == partner_user_id
|
||||
|
||||
partner_user = PartnerUser.get_by(
|
||||
partner_id=get_proton_partner().id, user_id=sl_user_2.id
|
||||
)
|
||||
assert partner_user is None
|
||||
|
||||
|
||||
def test_link_account_with_proton_account_different_address_and_linked_to_other_user(
|
||||
flask_client,
|
||||
):
|
||||
# In this scenario we have:
|
||||
# - PartnerUser (foo@partner)
|
||||
# - SimpleLoginUser1 (bar@somethingelse)
|
||||
# - SimpleLoginUser2 (other@somethingelse) linked with foo@partner
|
||||
# We will unlink SimpleLoginUser2 and link SimpleLoginUser1 with foo@partner
|
||||
partner_user_id = random_string()
|
||||
link_request = random_link_request(
|
||||
external_user_id=partner_user_id, email=random_email()
|
||||
)
|
||||
sl_user_1 = create_user(random_email())
|
||||
sl_user_2 = create_user_for_partner(
|
||||
partner_user_id, email=random_email()
|
||||
) # User already linked with the proton account
|
||||
|
||||
res = process_link_case(link_request, sl_user_1, get_proton_partner())
|
||||
assert res.user.id == sl_user_1.id
|
||||
assert res.user.email == sl_user_1.email
|
||||
assert res.strategy == "Link"
|
||||
|
||||
partner_user_1 = PartnerUser.get_by(
|
||||
user_id=sl_user_1.id, partner_id=get_proton_partner().id
|
||||
)
|
||||
assert partner_user_1 is not None
|
||||
assert partner_user_1.partner_email == sl_user_2.email
|
||||
assert partner_user_1.partner_id == get_proton_partner().id
|
||||
assert partner_user_1.external_user_id == partner_user_id
|
||||
|
||||
partner_user_2 = PartnerUser.get_by(
|
||||
user_id=sl_user_2.id, partner_id=get_proton_partner().id
|
||||
)
|
||||
assert partner_user_2 is None
|
||||
|
||||
|
||||
def test_cannot_create_instance_of_base_strategy():
|
||||
with pytest.raises(Exception):
|
||||
ClientMergeStrategy(random_link_request(), None, get_proton_partner())
|
||||
|
||||
|
||||
def test_ensure_partner_user_exists_for_user_raises_exception_when_linked_to_another_partner():
|
||||
# Setup test data:
|
||||
# - partner_1
|
||||
# - partner_2
|
||||
# - user
|
||||
user_email = random_email()
|
||||
user = create_user(user_email)
|
||||
external_id_1 = random_string()
|
||||
partner_1 = Partner.create(
|
||||
name=random_string(),
|
||||
contact_email=random_email(),
|
||||
)
|
||||
external_id_2 = random_string()
|
||||
partner_2 = Partner.create(
|
||||
name=random_string(),
|
||||
contact_email=random_email(),
|
||||
)
|
||||
|
||||
# Link user with partner_1
|
||||
ensure_partner_user_exists_for_user(
|
||||
PartnerLinkRequest(
|
||||
name=random_string(),
|
||||
email=user_email,
|
||||
external_user_id=external_id_1,
|
||||
plan=SLPlan(type=SLPlanType.Free, expiration=None),
|
||||
from_partner=False,
|
||||
),
|
||||
user,
|
||||
partner_1,
|
||||
)
|
||||
|
||||
# Try to link user with partner_2 and confirm the exception
|
||||
with pytest.raises(AccountAlreadyLinkedToAnotherPartnerException):
|
||||
ensure_partner_user_exists_for_user(
|
||||
PartnerLinkRequest(
|
||||
name=random_string(),
|
||||
email=user_email,
|
||||
external_user_id=external_id_2,
|
||||
plan=SLPlan(type=SLPlanType.Free, expiration=None),
|
||||
from_partner=False,
|
||||
),
|
||||
user,
|
||||
partner_2,
|
||||
)
|
||||
|
||||
|
||||
def test_link_account_with_uppercase(flask_client):
|
||||
# In this scenario we have:
|
||||
# - PartnerUser (email1@partner)
|
||||
# - SimpleLoginUser registered with email1@proton
|
||||
# We will try to link both accounts with an uppercase email
|
||||
|
||||
email = random_email()
|
||||
partner_user_id = random_string()
|
||||
link_request = random_link_request(
|
||||
external_user_id=partner_user_id, email=email.upper()
|
||||
)
|
||||
user = create_user(email)
|
||||
|
||||
res = process_link_case(link_request, user, get_proton_partner())
|
||||
assert res is not None
|
||||
assert res.user is not None
|
||||
assert res.user.id == user.id
|
||||
assert res.user.email == email
|
||||
assert res.strategy == "Link"
|
||||
|
||||
partner_user = PartnerUser.get_by(
|
||||
partner_id=get_proton_partner().id, user_id=user.id
|
||||
)
|
||||
assert partner_user.partner_id == get_proton_partner().id
|
||||
assert partner_user.external_user_id == partner_user_id
|
126
app/tests/test_alias_utils.py
Normal file
126
app/tests/test_alias_utils.py
Normal file
@ -0,0 +1,126 @@
|
||||
from typing import List
|
||||
|
||||
from app.alias_utils import (
|
||||
delete_alias,
|
||||
check_alias_prefix,
|
||||
get_user_if_alias_would_auto_create,
|
||||
try_auto_create,
|
||||
)
|
||||
from app.config import ALIAS_DOMAINS
|
||||
from app.db import Session
|
||||
from app.models import (
|
||||
Alias,
|
||||
DeletedAlias,
|
||||
CustomDomain,
|
||||
AutoCreateRule,
|
||||
Directory,
|
||||
DirectoryMailbox,
|
||||
User,
|
||||
)
|
||||
from tests.utils import create_new_user, random_domain, random_token
|
||||
|
||||
|
||||
def test_delete_alias(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
assert Alias.get_by(email=alias.email)
|
||||
|
||||
delete_alias(alias, user)
|
||||
assert Alias.get_by(email=alias.email) is None
|
||||
assert DeletedAlias.get_by(email=alias.email)
|
||||
|
||||
|
||||
def test_delete_alias_already_in_trash(flask_client):
|
||||
"""delete an alias that's already in alias trash"""
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
# add the alias to global trash
|
||||
Session.add(DeletedAlias(email=alias.email))
|
||||
Session.commit()
|
||||
|
||||
delete_alias(alias, user)
|
||||
assert Alias.get_by(email=alias.email) is None
|
||||
|
||||
|
||||
def test_check_alias_prefix(flask_client):
|
||||
assert check_alias_prefix("ab-cd_")
|
||||
assert not check_alias_prefix("")
|
||||
assert not check_alias_prefix("éè")
|
||||
assert not check_alias_prefix("a b")
|
||||
assert not check_alias_prefix("+👌")
|
||||
assert not check_alias_prefix("too-long" * 10)
|
||||
|
||||
|
||||
def get_auto_create_alias_tests(user: User) -> List:
|
||||
user.lifetime = True
|
||||
catchall = CustomDomain.create(
|
||||
user_id=user.id,
|
||||
catch_all=True,
|
||||
domain=random_domain(),
|
||||
verified=True,
|
||||
flush=True,
|
||||
)
|
||||
no_catchall = CustomDomain.create(
|
||||
user_id=user.id,
|
||||
catch_all=False,
|
||||
domain=random_domain(),
|
||||
verified=True,
|
||||
flush=True,
|
||||
)
|
||||
no_catchall_with_rule = CustomDomain.create(
|
||||
user_id=user.id,
|
||||
catch_all=False,
|
||||
domain=random_domain(),
|
||||
verified=True,
|
||||
flush=True,
|
||||
)
|
||||
AutoCreateRule.create(
|
||||
custom_domain_id=no_catchall_with_rule.id,
|
||||
order=0,
|
||||
regex="ok-.*",
|
||||
flush=True,
|
||||
)
|
||||
dir_name = random_token()
|
||||
directory = Directory.create(name=dir_name, user_id=user.id, flush=True)
|
||||
DirectoryMailbox.create(
|
||||
directory_id=directory.id, mailbox_id=user.default_mailbox_id, flush=True
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
return [
|
||||
(f"nonexistant@{catchall.domain}", True),
|
||||
(f"nonexistant@{no_catchall.domain}", False),
|
||||
(f"nonexistant@{no_catchall_with_rule.domain}", False),
|
||||
(f"ok-nonexistant@{no_catchall_with_rule.domain}", True),
|
||||
(f"{dir_name}+something@nowhere.net", False),
|
||||
(f"{dir_name}#something@nowhere.net", False),
|
||||
(f"{dir_name}/something@nowhere.net", False),
|
||||
(f"{dir_name}+something@{ALIAS_DOMAINS[0]}", True),
|
||||
(f"{dir_name}#something@{ALIAS_DOMAINS[0]}", True),
|
||||
(f"{dir_name}/something@{ALIAS_DOMAINS[0]}", True),
|
||||
]
|
||||
|
||||
|
||||
def test_get_user_if_alias_would_auto_create(flask_client):
|
||||
user = create_new_user()
|
||||
for test_id, (address, expected_ok) in enumerate(get_auto_create_alias_tests(user)):
|
||||
result = get_user_if_alias_would_auto_create(address)
|
||||
if expected_ok:
|
||||
assert (
|
||||
isinstance(result, User) and result.id == user.id
|
||||
), f"Case {test_id} - Failed address {address}"
|
||||
else:
|
||||
assert not result, f"Case {test_id} - Failed address {address}"
|
||||
|
||||
|
||||
def test_auto_create_alias(flask_client):
|
||||
user = create_new_user()
|
||||
for test_id, (address, expected_ok) in enumerate(get_auto_create_alias_tests(user)):
|
||||
result = try_auto_create(address)
|
||||
if expected_ok:
|
||||
assert result, f"Case {test_id} - Failed address {address}"
|
||||
else:
|
||||
assert result is None, f"Case {test_id} - Failed address {address}"
|
13
app/tests/test_config.py
Normal file
13
app/tests/test_config.py
Normal file
@ -0,0 +1,13 @@
|
||||
import pytest
|
||||
|
||||
from app.config import sl_getenv
|
||||
|
||||
|
||||
def test_sl_getenv(monkeypatch):
|
||||
monkeypatch.setenv("SL_KEY_1", '["domain_1"]')
|
||||
assert sl_getenv("SL_KEY_1") == ["domain_1"]
|
||||
|
||||
assert sl_getenv("SL_KEY_2", default_factory=list) == []
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
sl_getenv("SL_KEY_3")
|
38
app/tests/test_cron.py
Normal file
38
app/tests/test_cron.py
Normal file
@ -0,0 +1,38 @@
|
||||
import arrow
|
||||
|
||||
from app.models import CoinbaseSubscription, ApiToCookieToken, ApiKey
|
||||
from cron import notify_manual_sub_end, delete_expired_tokens
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def test_notify_manual_sub_end(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
CoinbaseSubscription.create(
|
||||
user_id=user.id, end_at=arrow.now().shift(days=13, hours=2), commit=True
|
||||
)
|
||||
|
||||
notify_manual_sub_end()
|
||||
|
||||
|
||||
def test_cleanup_tokens(flask_client):
|
||||
user = create_new_user()
|
||||
api_key = ApiKey.create(
|
||||
user_id=user.id,
|
||||
commit=True,
|
||||
)
|
||||
id_to_clean = ApiToCookieToken.create(
|
||||
user_id=user.id,
|
||||
api_key_id=api_key.id,
|
||||
commit=True,
|
||||
created_at=arrow.now().shift(days=-1),
|
||||
).id
|
||||
|
||||
id_to_keep = ApiToCookieToken.create(
|
||||
user_id=user.id,
|
||||
api_key_id=api_key.id,
|
||||
commit=True,
|
||||
).id
|
||||
delete_expired_tokens()
|
||||
assert ApiToCookieToken.get(id_to_clean) is None
|
||||
assert ApiToCookieToken.get(id_to_keep) is not None
|
48
app/tests/test_dns_utils.py
Normal file
48
app/tests/test_dns_utils.py
Normal file
@ -0,0 +1,48 @@
|
||||
from app.dns_utils import (
|
||||
get_mx_domains,
|
||||
get_spf_domain,
|
||||
get_txt_record,
|
||||
is_mx_equivalent,
|
||||
)
|
||||
|
||||
# use our own domain for test
|
||||
_DOMAIN = "simplelogin.io"
|
||||
|
||||
|
||||
def test_get_mx_domains():
|
||||
r = get_mx_domains(_DOMAIN)
|
||||
|
||||
assert len(r) > 0
|
||||
|
||||
for x in r:
|
||||
assert x[0] > 0
|
||||
assert x[1]
|
||||
|
||||
|
||||
def test_get_spf_domain():
|
||||
r = get_spf_domain(_DOMAIN)
|
||||
assert r == ["simplelogin.co"]
|
||||
|
||||
|
||||
def test_get_txt_record():
|
||||
r = get_txt_record(_DOMAIN)
|
||||
assert len(r) > 0
|
||||
|
||||
|
||||
def test_is_mx_equivalent():
|
||||
assert is_mx_equivalent([], [])
|
||||
assert is_mx_equivalent([(1, "domain")], [(1, "domain")])
|
||||
assert is_mx_equivalent(
|
||||
[(10, "domain1"), (20, "domain2")], [(10, "domain1"), (20, "domain2")]
|
||||
)
|
||||
assert is_mx_equivalent(
|
||||
[(5, "domain1"), (10, "domain2")], [(10, "domain1"), (20, "domain2")]
|
||||
)
|
||||
assert is_mx_equivalent(
|
||||
[(5, "domain1"), (10, "domain2"), (20, "domain3")],
|
||||
[(10, "domain1"), (20, "domain2")],
|
||||
)
|
||||
assert not is_mx_equivalent(
|
||||
[(5, "domain1"), (10, "domain2")],
|
||||
[(10, "domain1"), (20, "domain2"), (20, "domain3")],
|
||||
)
|
310
app/tests/test_email_handler.py
Normal file
310
app/tests/test_email_handler.py
Normal file
@ -0,0 +1,310 @@
|
||||
import random
|
||||
from email.message import EmailMessage
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from aiosmtpd.smtp import Envelope
|
||||
|
||||
import email_handler
|
||||
from app import config
|
||||
from app.config import EMAIL_DOMAIN, ALERT_DMARC_FAILED_REPLY_PHASE
|
||||
from app.db import Session
|
||||
from app.email import headers, status
|
||||
from app.email_utils import generate_verp_email
|
||||
from app.mail_sender import mail_sender
|
||||
from app.models import (
|
||||
Alias,
|
||||
AuthorizedAddress,
|
||||
IgnoredEmail,
|
||||
EmailLog,
|
||||
Notification,
|
||||
VerpType,
|
||||
Contact,
|
||||
SentAlert,
|
||||
)
|
||||
from email_handler import (
|
||||
get_mailbox_from_mail_from,
|
||||
should_ignore,
|
||||
is_automatic_out_of_office,
|
||||
)
|
||||
from tests.utils import load_eml_file, create_new_user, random_email
|
||||
|
||||
|
||||
def test_get_mailbox_from_mail_from(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
mb = get_mailbox_from_mail_from(user.email, alias)
|
||||
assert mb.email == user.email
|
||||
|
||||
mb = get_mailbox_from_mail_from("unauthorized@gmail.com", alias)
|
||||
assert mb is None
|
||||
|
||||
# authorized address
|
||||
AuthorizedAddress.create(
|
||||
user_id=user.id,
|
||||
mailbox_id=user.default_mailbox_id,
|
||||
email="unauthorized@gmail.com",
|
||||
commit=True,
|
||||
)
|
||||
mb = get_mailbox_from_mail_from("unauthorized@gmail.com", alias)
|
||||
assert mb.email == user.email
|
||||
|
||||
|
||||
def test_should_ignore(flask_client):
|
||||
assert should_ignore("mail_from", []) is False
|
||||
|
||||
assert not should_ignore("mail_from", ["rcpt_to"])
|
||||
IgnoredEmail.create(mail_from="mail_from", rcpt_to="rcpt_to", commit=True)
|
||||
assert should_ignore("mail_from", ["rcpt_to"])
|
||||
|
||||
|
||||
def test_is_automatic_out_of_office():
|
||||
msg = EmailMessage()
|
||||
assert not is_automatic_out_of_office(msg)
|
||||
|
||||
msg[headers.AUTO_SUBMITTED] = "auto-replied"
|
||||
assert is_automatic_out_of_office(msg)
|
||||
|
||||
del msg[headers.AUTO_SUBMITTED]
|
||||
assert not is_automatic_out_of_office(msg)
|
||||
|
||||
msg[headers.AUTO_SUBMITTED] = "auto-generated"
|
||||
assert is_automatic_out_of_office(msg)
|
||||
|
||||
|
||||
def test_dmarc_forward_quarantine(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
msg = load_eml_file("dmarc_quarantine.eml", {"alias_email": alias.email})
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = msg["from"]
|
||||
envelope.rcpt_tos = [msg["to"]]
|
||||
result = email_handler.handle(envelope, msg)
|
||||
assert result == status.E215
|
||||
email_logs = (
|
||||
EmailLog.filter_by(user_id=user.id, alias_id=alias.id)
|
||||
.order_by(EmailLog.id.desc())
|
||||
.all()
|
||||
)
|
||||
assert len(email_logs) == 1
|
||||
email_log = email_logs[0]
|
||||
assert email_log.blocked
|
||||
assert email_log.refused_email_id
|
||||
notifications = Notification.filter_by(user_id=user.id).all()
|
||||
assert len(notifications) == 1
|
||||
assert f"{alias.email} has a new mail in quarantine" == notifications[0].title
|
||||
|
||||
|
||||
def test_gmail_dmarc_softfail(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
msg = load_eml_file("dmarc_gmail_softfail.eml", {"alias_email": alias.email})
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = msg["from"]
|
||||
envelope.rcpt_tos = [msg["to"]]
|
||||
result = email_handler.handle(envelope, msg)
|
||||
assert result == status.E200
|
||||
# Enable when we can verify that the actual message sent has this content
|
||||
# payload = msg.get_payload()
|
||||
# assert payload.find("failed anti-phishing checks") > -1
|
||||
|
||||
|
||||
def test_prevent_5xx_from_spf(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
msg = load_eml_file(
|
||||
"5xx_overwrite_spf.eml",
|
||||
{"alias_email": alias.email, "spf_result": "R_SPF_FAIL"},
|
||||
)
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = msg["from"]
|
||||
# Ensure invalid email log
|
||||
envelope.rcpt_tos = [generate_verp_email(VerpType.bounce_forward, 99999999999999)]
|
||||
result = email_handler.MailHandler()._handle(envelope, msg)
|
||||
assert status.E216 == result
|
||||
|
||||
|
||||
def test_preserve_5xx_with_valid_spf(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
msg = load_eml_file(
|
||||
"5xx_overwrite_spf.eml",
|
||||
{"alias_email": alias.email, "spf_result": "R_SPF_ALLOW"},
|
||||
)
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = msg["from"]
|
||||
# Ensure invalid email log
|
||||
envelope.rcpt_tos = [generate_verp_email(VerpType.bounce_forward, 99999999999999)]
|
||||
result = email_handler.MailHandler()._handle(envelope, msg)
|
||||
assert status.E512 == result
|
||||
|
||||
|
||||
def test_preserve_5xx_with_no_header(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
msg = load_eml_file(
|
||||
"no_spamd_header.eml",
|
||||
{"alias_email": alias.email},
|
||||
)
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = msg["from"]
|
||||
# Ensure invalid email log
|
||||
envelope.rcpt_tos = [generate_verp_email(VerpType.bounce_forward, 99999999999999)]
|
||||
result = email_handler.MailHandler()._handle(envelope, msg)
|
||||
assert status.E512 == result
|
||||
|
||||
|
||||
def generate_dmarc_result() -> List:
|
||||
return ["DMARC_POLICY_QUARANTINE", "DMARC_POLICY_REJECT", "DMARC_POLICY_SOFTFAIL"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dmarc_result", generate_dmarc_result())
|
||||
def test_dmarc_reply_quarantine(flask_client, dmarc_result):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
contact = Contact.create(
|
||||
user_id=alias.user_id,
|
||||
alias_id=alias.id,
|
||||
website_email="random-{}@nowhere.net".format(int(random.random())),
|
||||
name="Name {}".format(int(random.random())),
|
||||
reply_email="random-{}@{}".format(random.random(), EMAIL_DOMAIN),
|
||||
)
|
||||
Session.commit()
|
||||
msg = load_eml_file(
|
||||
"dmarc_reply_check.eml",
|
||||
{
|
||||
"alias_email": alias.email,
|
||||
"contact_email": contact.reply_email,
|
||||
"dmarc_result": dmarc_result,
|
||||
},
|
||||
)
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = msg["from"]
|
||||
envelope.rcpt_tos = [msg["to"]]
|
||||
result = email_handler.handle(envelope, msg)
|
||||
assert result == status.E215
|
||||
alerts = SentAlert.filter_by(
|
||||
user_id=user.id, alert_type=ALERT_DMARC_FAILED_REPLY_PHASE
|
||||
).all()
|
||||
assert len(alerts) == 1
|
||||
|
||||
|
||||
def test_add_alias_to_header_if_needed():
|
||||
msg = EmailMessage()
|
||||
user = create_new_user()
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
|
||||
assert msg[headers.TO] is None
|
||||
|
||||
email_handler.add_alias_to_header_if_needed(msg, alias)
|
||||
|
||||
assert msg[headers.TO] == alias.email
|
||||
|
||||
|
||||
def test_append_alias_to_header_if_needed_existing_to():
|
||||
msg = EmailMessage()
|
||||
original_to = "noone@nowhere.no"
|
||||
msg[headers.TO] = original_to
|
||||
user = create_new_user()
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
email_handler.add_alias_to_header_if_needed(msg, alias)
|
||||
assert msg[headers.TO] == f"{original_to}, {alias.email}"
|
||||
|
||||
|
||||
def test_avoid_add_to_header_already_present():
|
||||
msg = EmailMessage()
|
||||
user = create_new_user()
|
||||
alias = Alias.filter_by(user_id=user.id).first()
|
||||
msg[headers.TO] = alias.email
|
||||
email_handler.add_alias_to_header_if_needed(msg, alias)
|
||||
assert msg[headers.TO] == alias.email
|
||||
|
||||
|
||||
def test_avoid_add_to_header_already_present_in_cc():
|
||||
msg = EmailMessage()
|
||||
create_new_user()
|
||||
alias = Alias.first()
|
||||
msg[headers.CC] = alias.email
|
||||
email_handler.add_alias_to_header_if_needed(msg, alias)
|
||||
assert msg[headers.TO] is None
|
||||
assert msg[headers.CC] == alias.email
|
||||
|
||||
|
||||
def test_email_sent_to_noreply(flask_client):
|
||||
msg = EmailMessage()
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = "from@domain.test"
|
||||
envelope.rcpt_tos = [config.NOREPLY]
|
||||
result = email_handler.handle(envelope, msg)
|
||||
assert result == status.E200
|
||||
|
||||
|
||||
def test_email_sent_to_noreplies(flask_client):
|
||||
msg = EmailMessage()
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = "from@domain.test"
|
||||
config.NOREPLIES = ["other-no-reply@sl.test"]
|
||||
|
||||
envelope.rcpt_tos = ["other-no-reply@sl.test"]
|
||||
result = email_handler.handle(envelope, msg)
|
||||
assert result == status.E200
|
||||
|
||||
# NOREPLY isn't used anymore
|
||||
envelope.rcpt_tos = [config.NOREPLY]
|
||||
result = email_handler.handle(envelope, msg)
|
||||
assert result == status.E515
|
||||
|
||||
|
||||
def test_references_header(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
msg = load_eml_file("reference_encoded.eml", {"alias_email": alias.email})
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = "somewhere@rainbow.com"
|
||||
envelope.rcpt_tos = [alias.email]
|
||||
result = email_handler.handle(envelope, msg)
|
||||
assert result == status.E200
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_replace_contacts_and_user_in_reply_phase(flask_client):
|
||||
user = create_new_user()
|
||||
user.replace_reverse_alias = True
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.flush()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=random_email(),
|
||||
reply_email=f"{random.random()}@{EMAIL_DOMAIN}",
|
||||
commit=True,
|
||||
)
|
||||
contact_real_mail = contact.website_email
|
||||
contact2 = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=random_email(),
|
||||
reply_email=f"{random.random()}@{EMAIL_DOMAIN}",
|
||||
commit=True,
|
||||
)
|
||||
contact2_real_mail = contact2.website_email
|
||||
msg = load_eml_file(
|
||||
"replacement_on_reply_phase.eml",
|
||||
{
|
||||
"contact_reply_email": contact.reply_email,
|
||||
"other_contact_reply_email": contact2.reply_email,
|
||||
},
|
||||
)
|
||||
envelope = Envelope()
|
||||
envelope.mail_from = alias.mailbox.email
|
||||
envelope.rcpt_tos = [contact.reply_email]
|
||||
result = email_handler.handle(envelope, msg)
|
||||
assert result == status.E200
|
||||
sent_mails = mail_sender.get_stored_emails()
|
||||
assert len(sent_mails) == 1
|
||||
payload = sent_mails[0].msg.get_payload()[0].get_payload()
|
||||
assert payload.find("Contact is {}".format(contact_real_mail)) > -1
|
||||
assert payload.find("Other contact is {}".format(contact2_real_mail)) > -1
|
799
app/tests/test_email_utils.py
Normal file
799
app/tests/test_email_utils.py
Normal file
@ -0,0 +1,799 @@
|
||||
import email
|
||||
import os
|
||||
from email.message import EmailMessage
|
||||
from email.utils import formataddr
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
|
||||
from app.config import MAX_ALERT_24H, EMAIL_DOMAIN, ROOT_DIR
|
||||
from app.db import Session
|
||||
from app.email_utils import (
|
||||
get_email_domain_part,
|
||||
can_create_directory_for_address,
|
||||
email_can_be_used_as_mailbox,
|
||||
delete_header,
|
||||
add_or_replace_header,
|
||||
send_email_with_rate_control,
|
||||
copy,
|
||||
get_spam_from_header,
|
||||
get_header_from_bounce,
|
||||
is_valid_email,
|
||||
add_header,
|
||||
generate_reply_email,
|
||||
normalize_reply_email,
|
||||
get_encoding,
|
||||
encode_text,
|
||||
EmailEncoding,
|
||||
replace,
|
||||
should_disable,
|
||||
decode_text,
|
||||
parse_id_from_bounce,
|
||||
get_queue_id,
|
||||
should_ignore_bounce,
|
||||
get_header_unicode,
|
||||
parse_full_address,
|
||||
get_orig_message_from_bounce,
|
||||
get_mailbox_bounce_info,
|
||||
is_invalid_mailbox_domain,
|
||||
generate_verp_email,
|
||||
get_verp_info_from_email,
|
||||
sl_formataddr,
|
||||
)
|
||||
from app.models import (
|
||||
CustomDomain,
|
||||
Alias,
|
||||
Contact,
|
||||
EmailLog,
|
||||
IgnoreBounceSender,
|
||||
InvalidMailboxDomain,
|
||||
VerpType,
|
||||
)
|
||||
|
||||
# flake8: noqa: E101, W191
|
||||
from tests.utils import login, load_eml_file, create_new_user, random_domain
|
||||
|
||||
|
||||
def test_get_email_domain_part():
|
||||
assert get_email_domain_part("ab@cd.com") == "cd.com"
|
||||
|
||||
|
||||
def test_email_belongs_to_alias_domains():
|
||||
# default alias domain
|
||||
assert can_create_directory_for_address("ab@sl.local")
|
||||
assert not can_create_directory_for_address("ab@not-exist.local")
|
||||
|
||||
assert can_create_directory_for_address("hey@d1.test")
|
||||
assert not can_create_directory_for_address("hey@d3.test")
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"GITHUB_ACTIONS_TEST" in os.environ,
|
||||
reason="this test requires DNS lookup that does not work on Github CI",
|
||||
)
|
||||
def test_can_be_used_as_personal_email(flask_client):
|
||||
# default alias domain
|
||||
assert not email_can_be_used_as_mailbox("ab@sl.local")
|
||||
assert not email_can_be_used_as_mailbox("hey@d1.test")
|
||||
|
||||
# custom domain
|
||||
domain = random_domain()
|
||||
user = create_new_user()
|
||||
CustomDomain.create(user_id=user.id, domain=domain, verified=True, commit=True)
|
||||
assert not email_can_be_used_as_mailbox(f"hey@{domain}")
|
||||
|
||||
# disposable domain
|
||||
disposable_domain = random_domain()
|
||||
InvalidMailboxDomain.create(domain=disposable_domain, commit=True)
|
||||
assert not email_can_be_used_as_mailbox(f"abcd@{disposable_domain}")
|
||||
# subdomain will not work
|
||||
assert not email_can_be_used_as_mailbox("abcd@sub.{disposable_domain}")
|
||||
# valid domains should not be affected
|
||||
assert email_can_be_used_as_mailbox("abcd@protonmail.com")
|
||||
assert email_can_be_used_as_mailbox("abcd@gmail.com")
|
||||
|
||||
|
||||
def test_delete_header():
|
||||
msg = EmailMessage()
|
||||
assert msg._headers == []
|
||||
|
||||
msg["H"] = "abcd"
|
||||
msg["H"] = "xyzt"
|
||||
|
||||
assert msg._headers == [("H", "abcd"), ("H", "xyzt")]
|
||||
|
||||
delete_header(msg, "H")
|
||||
assert msg._headers == []
|
||||
|
||||
|
||||
def test_add_or_replace_header():
|
||||
msg = EmailMessage()
|
||||
msg["H"] = "abcd"
|
||||
msg["H"] = "xyzt"
|
||||
assert msg._headers == [("H", "abcd"), ("H", "xyzt")]
|
||||
|
||||
add_or_replace_header(msg, "H", "new")
|
||||
assert msg._headers == [("H", "new")]
|
||||
|
||||
|
||||
def test_parse_full_address():
|
||||
# only email
|
||||
assert parse_full_address("abcd@gmail.com") == (
|
||||
"",
|
||||
"abcd@gmail.com",
|
||||
)
|
||||
|
||||
# ascii address
|
||||
assert parse_full_address("First Last <abcd@gmail.com>") == (
|
||||
"First Last",
|
||||
"abcd@gmail.com",
|
||||
)
|
||||
|
||||
# Handle quote
|
||||
assert parse_full_address('"First Last" <abcd@gmail.com>') == (
|
||||
"First Last",
|
||||
"abcd@gmail.com",
|
||||
)
|
||||
|
||||
# UTF-8 charset
|
||||
assert parse_full_address("=?UTF-8?B?TmjGoW4gTmd1eeG7hW4=?= <abcd@gmail.com>") == (
|
||||
"Nhơn Nguyễn",
|
||||
"abcd@gmail.com",
|
||||
)
|
||||
|
||||
# iso-8859-1 charset
|
||||
assert parse_full_address("=?iso-8859-1?q?p=F6stal?= <abcd@gmail.com>") == (
|
||||
"pöstal",
|
||||
"abcd@gmail.com",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
parse_full_address("https://ab.cd")
|
||||
|
||||
|
||||
def test_send_email_with_rate_control(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
for _ in range(MAX_ALERT_24H):
|
||||
assert send_email_with_rate_control(
|
||||
user, "test alert type", "abcd@gmail.com", "subject", "plaintext"
|
||||
)
|
||||
assert not send_email_with_rate_control(
|
||||
user, "test alert type", "abcd@gmail.com", "subject", "plaintext"
|
||||
)
|
||||
|
||||
|
||||
def test_get_spam_from_header():
|
||||
is_spam, _ = get_spam_from_header(
|
||||
"""No, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID,
|
||||
DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
|
||||
URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2"""
|
||||
)
|
||||
assert not is_spam
|
||||
|
||||
is_spam, _ = get_spam_from_header(
|
||||
"""Yes, score=-0.1 required=5.0 tests=DKIM_SIGNED,DKIM_VALID,
|
||||
DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
|
||||
URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2"""
|
||||
)
|
||||
assert is_spam
|
||||
|
||||
# the case where max_score is less than the default used by SpamAssassin
|
||||
is_spam, _ = get_spam_from_header(
|
||||
"""No, score=6 required=10.0 tests=DKIM_SIGNED,DKIM_VALID,
|
||||
DKIM_VALID_AU,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2,SPF_PASS,
|
||||
URIBL_BLOCKED autolearn=unavailable autolearn_force=no version=3.4.2""",
|
||||
max_score=5,
|
||||
)
|
||||
assert is_spam
|
||||
|
||||
|
||||
def test_get_header_from_bounce():
|
||||
# this is an actual bounce report from iCloud anonymized
|
||||
msg_str = """Received: by mx1.simplelogin.co (Postfix)
|
||||
id 9988776655; Mon, 24 Aug 2020 06:20:07 +0000 (UTC)
|
||||
Date: Mon, 24 Aug 2020 06:20:07 +0000 (UTC)
|
||||
From: MAILER-DAEMON@bounce.simplelogin.io (Mail Delivery System)
|
||||
Subject: Undelivered Mail Returned to Sender
|
||||
To: reply+longstring@simplelogin.co
|
||||
Auto-Submitted: auto-replied
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type=delivery-status;
|
||||
boundary="XXYYZZTT.1598250007/mx1.simplelogin.co"
|
||||
Content-Transfer-Encoding: 8bit
|
||||
Message-Id: <20200824062007.9988776655@mx1.simplelogin.co>
|
||||
|
||||
This is a MIME-encapsulated message.
|
||||
|
||||
--XXYYZZTT.1598250007/mx1.simplelogin.co
|
||||
Content-Description: Notification
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
This is the mail system at host mx1.simplelogin.co.
|
||||
|
||||
I'm sorry to have to inform you that your message could not
|
||||
be delivered to one or more recipients. It's attached below.
|
||||
|
||||
For further assistance, please send mail to <postmaster@simplelogin.io>
|
||||
|
||||
If you do so, please include this problem report. You can
|
||||
delete your own text from the attached returned message.
|
||||
|
||||
The mail system
|
||||
|
||||
<something@icloud.com>: host mx01.mail.icloud.com[17.57.154.6] said:
|
||||
554 5.7.1 [CS01] Message rejected due to local policy. Please visit
|
||||
https://support.apple.com/en-us/HT204137 (in reply to end of DATA command)
|
||||
|
||||
--XXYYZZTT.1598250007/mx1.simplelogin.co
|
||||
Content-Description: Delivery report
|
||||
Content-Type: message/delivery-status
|
||||
|
||||
Reporting-MTA: dns; mx1.simplelogin.co
|
||||
X-Postfix-Queue-ID: XXYYZZTT
|
||||
X-Postfix-Sender: rfc822; reply+longstring@simplelogin.co
|
||||
Arrival-Date: Mon, 24 Aug 2020 06:20:04 +0000 (UTC)
|
||||
|
||||
Final-Recipient: rfc822; something@icloud.com
|
||||
Original-Recipient: rfc822;something@icloud.com
|
||||
Action: failed
|
||||
Status: 5.7.1
|
||||
Remote-MTA: dns; mx01.mail.icloud.com
|
||||
Diagnostic-Code: smtp; 554 5.7.1 [CS01] Message rejected due to local policy.
|
||||
Please visit https://support.apple.com/en-us/HT204137
|
||||
|
||||
--XXYYZZTT.1598250007/mx1.simplelogin.co
|
||||
Content-Description: Undelivered Message Headers
|
||||
Content-Type: text/rfc822-headers
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Return-Path: <reply+longstring@simplelogin.co>
|
||||
X-SimpleLogin-Client-IP: 172.17.0.4
|
||||
Received: from [172.17.0.4] (unknown [172.17.0.4])
|
||||
by mx1.simplelogin.co (Postfix) with ESMTP id XXYYZZTT
|
||||
for <something@icloud.com>; Mon, 24 Aug 2020 06:20:04 +0000 (UTC)
|
||||
Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=91.241.74.242;
|
||||
helo=mail23-242.srv2.de; envelope-from=return@mailing.dhl.de;
|
||||
receiver=<UNKNOWN>
|
||||
Received: from mail23-242.srv2.de (mail23-242.srv2.de [91.241.74.242])
|
||||
(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits))
|
||||
(No client certificate requested)
|
||||
by mx1.simplelogin.co (Postfix) with ESMTPS id B7D123F1C6
|
||||
for <dhl@something.com>; Mon, 24 Aug 2020 06:20:03 +0000 (UTC)
|
||||
Message-ID: <368362807.12707001.1598249997169@rnd-04.broadmail.live>
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/signed; protocol="application/pkcs7-signature";
|
||||
micalg=sha-256;
|
||||
boundary="----=_Part_12707000_248822956.1598249997168"
|
||||
Date: Mon, 24 Aug 2020 08:19:57 +0200 (CEST)
|
||||
To: dhl@something.com
|
||||
Subject: Test subject
|
||||
X-ulpe:
|
||||
re-pO_5F8NoxrdpyqkmsptkpyTxDqB3osb7gfyo-41ZOK78E-3EOXXNLB-FKZPLZ@mailing.dhl.de
|
||||
List-Id: <1CZ4Z7YB-1DYLQB8.mailing.dhl.de>
|
||||
X-Report-Spam: complaints@episerver.com
|
||||
X-CSA-Complaints: whitelist-complaints@eco.de
|
||||
List-Unsubscribe-Post: List-Unsubscribe=One-Click
|
||||
mkaTechnicalID: 123456
|
||||
Feedback-ID: 1CZ4Z7YB:3EOXXNLB:episerver
|
||||
X-SimpleLogin-Type: Forward
|
||||
X-SimpleLogin-Mailbox-ID: 1234
|
||||
X-SimpleLogin-EmailLog-ID: 654321
|
||||
From: "DHL Paket - noreply@dhl.de"
|
||||
<reply+longstring@simplelogin.co>
|
||||
List-Unsubscribe: <mailto:unsubsribe@simplelogin.co?subject=123456=>
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=simplelogin.co;
|
||||
i=@simplelogin.co; q=dns/txt; s=dkim; t=1598250004; h=from : to;
|
||||
bh=nXVR9uziNfqtwyhq6gQLFJvFtdyQ8WY/w7c1mCaf7bg=;
|
||||
b=QY/Jb4ls0zFOqExWFkwW9ZOKNvkYPDsj74ar1LNm703kyL341KwX3rGnnicrLV7WxYo8+
|
||||
pBY0HO7OSAJEOqmYdagAlVouuFiBMUtS2Jw/jiPHzcuvunE9JFOZFRUnNMKrr099i10U4H9
|
||||
ZwE8i6lQzG6IMN4spjxJ2HCO8hiB3AU=
|
||||
|
||||
--XXYYZZTT.1598250007/mx1.simplelogin.co--
|
||||
|
||||
"""
|
||||
assert (
|
||||
get_header_from_bounce(
|
||||
email.message_from_string(msg_str), "X-SimpleLogin-Mailbox-ID"
|
||||
)
|
||||
== "1234"
|
||||
)
|
||||
assert (
|
||||
get_header_from_bounce(
|
||||
email.message_from_string(msg_str), "X-SimpleLogin-EmailLog-ID"
|
||||
)
|
||||
== "654321"
|
||||
)
|
||||
assert (
|
||||
get_header_from_bounce(email.message_from_string(msg_str), "Not-exist") is None
|
||||
)
|
||||
|
||||
|
||||
def test_is_valid_email():
|
||||
assert is_valid_email("abcd@gmail.com")
|
||||
assert not is_valid_email("")
|
||||
assert not is_valid_email(" ")
|
||||
assert not is_valid_email("with space@gmail.com")
|
||||
assert not is_valid_email("strange char !ç@gmail.com")
|
||||
assert not is_valid_email("emoji👌@gmail.com")
|
||||
|
||||
|
||||
def test_add_header_plain_text():
|
||||
msg = email.message_from_string(
|
||||
"""Content-Type: text/plain; charset=us-ascii
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Test-Header: Test-Value
|
||||
|
||||
coucou
|
||||
"""
|
||||
)
|
||||
new_msg = add_header(msg, "text header", "html header")
|
||||
assert "text header" in new_msg.as_string()
|
||||
assert "html header" not in new_msg.as_string()
|
||||
|
||||
|
||||
def test_add_header_html():
|
||||
msg = email.message_from_string(
|
||||
"""Content-Type: text/html; charset=us-ascii
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Test-Header: Test-Value
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
||||
</head>
|
||||
<body style="word-wrap: break-word;" class="">
|
||||
<b class="">bold</b>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
new_msg = add_header(msg, "text header", "html header")
|
||||
assert "Test-Header: Test-Value" in new_msg.as_string()
|
||||
assert "<table" in new_msg.as_string()
|
||||
assert "</table>" in new_msg.as_string()
|
||||
assert "html header" in new_msg.as_string()
|
||||
assert "text header" not in new_msg.as_string()
|
||||
|
||||
|
||||
def test_add_header_multipart_alternative():
|
||||
msg = email.message_from_string(
|
||||
"""Content-Type: multipart/alternative;
|
||||
boundary="foo"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Test-Header: Test-Value
|
||||
|
||||
--foo
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Type: text/plain;
|
||||
charset=us-ascii
|
||||
|
||||
bold
|
||||
|
||||
--foo
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Type: text/html;
|
||||
charset=us-ascii
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
||||
</head>
|
||||
<body style="word-wrap: break-word;" class="">
|
||||
<b class="">bold</b>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
new_msg = add_header(msg, "text header", "html header")
|
||||
assert "Test-Header: Test-Value" in new_msg.as_string()
|
||||
assert "<table" in new_msg.as_string()
|
||||
assert "</table>" in new_msg.as_string()
|
||||
assert "html header" in new_msg.as_string()
|
||||
assert "text header" in new_msg.as_string()
|
||||
|
||||
|
||||
def test_replace_no_encoding():
|
||||
msg = email.message_from_string(
|
||||
"""Content-Type: text/plain; charset=us-ascii
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Test-Header: Test-Value
|
||||
|
||||
old
|
||||
"""
|
||||
)
|
||||
new_msg = replace(msg, "old", "new")
|
||||
assert "new" in new_msg.as_string()
|
||||
assert "old" not in new_msg.as_string()
|
||||
|
||||
# headers are not affected
|
||||
assert "Test-Header: Test-Value" in new_msg.as_string()
|
||||
|
||||
|
||||
def test_replace_base64_encoding():
|
||||
# "b2xk" is "old" base64-encoded
|
||||
msg = email.message_from_string(
|
||||
"""Content-Type: text/plain; charset=us-ascii
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
b2xk
|
||||
"""
|
||||
)
|
||||
new_msg = replace(msg, "old", "new")
|
||||
# bmV3 is new base64 encoded
|
||||
assert "bmV3" in new_msg.as_string()
|
||||
assert "b2xk" not in new_msg.as_string()
|
||||
|
||||
|
||||
def test_replace_multipart_alternative():
|
||||
msg = email.message_from_string(
|
||||
"""Content-Type: multipart/alternative;
|
||||
boundary="foo"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Test-Header: Test-Value
|
||||
|
||||
--foo
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Type: text/plain; charset=us-ascii
|
||||
|
||||
old
|
||||
|
||||
--foo
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Type: text/html; charset=us-ascii
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
|
||||
</head>
|
||||
<body style="word-wrap: break-word;" class="">
|
||||
<b class="">old</b>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
)
|
||||
new_msg = replace(msg, "old", "new")
|
||||
# headers are not affected
|
||||
assert "Test-Header: Test-Value" in new_msg.as_string()
|
||||
|
||||
assert "new" in new_msg.as_string()
|
||||
assert "old" not in new_msg.as_string()
|
||||
|
||||
|
||||
def test_replace_str():
|
||||
msg = "a string"
|
||||
new_msg = replace(msg, "a", "still a")
|
||||
assert new_msg == "still a string"
|
||||
|
||||
|
||||
def test_generate_reply_email(flask_client):
|
||||
user = create_new_user()
|
||||
reply_email = generate_reply_email("test@example.org", user)
|
||||
assert reply_email.endswith(EMAIL_DOMAIN)
|
||||
|
||||
reply_email = generate_reply_email("", user)
|
||||
assert reply_email.endswith(EMAIL_DOMAIN)
|
||||
|
||||
|
||||
def test_generate_reply_email_include_sender_in_reverse_alias(flask_client):
|
||||
# user enables include_sender_in_reverse_alias
|
||||
user = create_new_user()
|
||||
user.include_sender_in_reverse_alias = True
|
||||
|
||||
reply_email = generate_reply_email("test@example.org", user)
|
||||
assert reply_email.startswith("test_at_example_org")
|
||||
assert reply_email.endswith(EMAIL_DOMAIN)
|
||||
|
||||
reply_email = generate_reply_email("", user)
|
||||
assert reply_email.endswith(EMAIL_DOMAIN)
|
||||
|
||||
reply_email = generate_reply_email("👌汉字@example.org", user)
|
||||
assert reply_email.startswith("yizi_at_example_org")
|
||||
|
||||
# make sure reply_email only contain lowercase
|
||||
reply_email = generate_reply_email("TEST@example.org", user)
|
||||
assert reply_email.startswith("test_at_example_org")
|
||||
|
||||
reply_email = generate_reply_email("test.dot@example.org", user)
|
||||
assert reply_email.startswith("test_dot_at_example_org")
|
||||
|
||||
|
||||
def test_normalize_reply_email(flask_client):
|
||||
assert normalize_reply_email("re+abcd@sl.local") == "re+abcd@sl.local"
|
||||
assert normalize_reply_email('re+"ab cd"@sl.local') == "re+_ab_cd_@sl.local"
|
||||
|
||||
|
||||
def test_get_encoding():
|
||||
msg = email.message_from_string("")
|
||||
assert get_encoding(msg) == EmailEncoding.NO
|
||||
|
||||
msg = email.message_from_string("Content-TRANSFER-encoding: Invalid")
|
||||
assert get_encoding(msg) == EmailEncoding.NO
|
||||
|
||||
msg = email.message_from_string("Content-TRANSFER-encoding: 7bit")
|
||||
assert get_encoding(msg) == EmailEncoding.NO
|
||||
|
||||
msg = email.message_from_string("Content-TRANSFER-encoding: 8bit")
|
||||
assert get_encoding(msg) == EmailEncoding.NO
|
||||
|
||||
msg = email.message_from_string("Content-TRANSFER-encoding: binary")
|
||||
assert get_encoding(msg) == EmailEncoding.NO
|
||||
|
||||
msg = email.message_from_string("Content-TRANSFER-encoding: quoted-printable")
|
||||
assert get_encoding(msg) == EmailEncoding.QUOTED
|
||||
|
||||
msg = email.message_from_string("Content-TRANSFER-encoding: base64")
|
||||
assert get_encoding(msg) == EmailEncoding.BASE64
|
||||
|
||||
|
||||
def test_encode_text():
|
||||
assert encode_text("") == ""
|
||||
assert encode_text("ascii") == "ascii"
|
||||
assert encode_text("ascii", EmailEncoding.BASE64) == "YXNjaWk="
|
||||
assert encode_text("ascii", EmailEncoding.QUOTED) == "ascii"
|
||||
|
||||
assert encode_text("mèo méo") == "mèo méo"
|
||||
assert encode_text("mèo méo", EmailEncoding.BASE64) == "bcOobyBtw6lv"
|
||||
assert encode_text("mèo méo", EmailEncoding.QUOTED) == "m=C3=A8o m=C3=A9o"
|
||||
|
||||
|
||||
def test_decode_text():
|
||||
assert decode_text("") == ""
|
||||
assert decode_text("ascii") == "ascii"
|
||||
|
||||
assert (
|
||||
decode_text(encode_text("ascii", EmailEncoding.BASE64), EmailEncoding.BASE64)
|
||||
== "ascii"
|
||||
)
|
||||
assert (
|
||||
decode_text(
|
||||
encode_text("mèo méo 🇪🇺", EmailEncoding.BASE64), EmailEncoding.BASE64
|
||||
)
|
||||
== "mèo méo 🇪🇺"
|
||||
)
|
||||
|
||||
assert (
|
||||
decode_text(encode_text("ascii", EmailEncoding.QUOTED), EmailEncoding.QUOTED)
|
||||
== "ascii"
|
||||
)
|
||||
assert (
|
||||
decode_text(
|
||||
encode_text("mèo méo 🇪🇺", EmailEncoding.QUOTED), EmailEncoding.QUOTED
|
||||
)
|
||||
== "mèo méo 🇪🇺"
|
||||
)
|
||||
|
||||
|
||||
def test_should_disable(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
assert not should_disable(alias)[0]
|
||||
|
||||
# create a lot of bounce on this alias
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
commit=True,
|
||||
)
|
||||
for _ in range(20):
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
commit=True,
|
||||
bounced=True,
|
||||
)
|
||||
|
||||
assert should_disable(alias)[0]
|
||||
|
||||
# should not affect another alias
|
||||
alias2 = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
assert not should_disable(alias2)[0]
|
||||
|
||||
|
||||
def test_should_disable_bounces_every_day(flask_client):
|
||||
"""if an alias has bounces every day at least 9 days in the last 10 days, disable alias"""
|
||||
user = login(flask_client)
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
assert not should_disable(alias)[0]
|
||||
|
||||
# create a lot of bounce on this alias
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
commit=True,
|
||||
)
|
||||
for i in range(9):
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
commit=True,
|
||||
bounced=True,
|
||||
created_at=arrow.now().shift(days=-i),
|
||||
)
|
||||
|
||||
assert should_disable(alias)[0]
|
||||
|
||||
|
||||
def test_should_disable_bounces_account(flask_client):
|
||||
"""if an account has more than 10 bounces every day for at least 5 days in the last 10 days, disable alias"""
|
||||
user = login(flask_client)
|
||||
alias = Alias.create_new_random(user)
|
||||
|
||||
Session.commit()
|
||||
|
||||
# create a lot of bounces on alias
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
for day in range(5):
|
||||
for _ in range(11):
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
commit=True,
|
||||
bounced=True,
|
||||
created_at=arrow.now().shift(days=-day),
|
||||
)
|
||||
|
||||
alias2 = Alias.create_new_random(user)
|
||||
assert should_disable(alias2)[0]
|
||||
|
||||
|
||||
def test_should_disable_bounce_consecutive_days(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email="contact@example.com",
|
||||
reply_email="rep@sl.local",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# create 6 bounce on this alias in the last 24h: alias is not disabled
|
||||
for _ in range(6):
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
commit=True,
|
||||
bounced=True,
|
||||
)
|
||||
assert not should_disable(alias)[0]
|
||||
|
||||
# create +10 bounces in the last 7 days: alias should be disabled
|
||||
for _ in range(11):
|
||||
EmailLog.create(
|
||||
user_id=user.id,
|
||||
contact_id=contact.id,
|
||||
alias_id=contact.alias_id,
|
||||
commit=True,
|
||||
bounced=True,
|
||||
created_at=arrow.now().shift(days=-3),
|
||||
)
|
||||
assert should_disable(alias)[0]
|
||||
|
||||
|
||||
def test_parse_id_from_bounce():
|
||||
assert parse_id_from_bounce("bounces+1234+@local") == 1234
|
||||
assert parse_id_from_bounce("anything+1234+@local") == 1234
|
||||
|
||||
|
||||
def test_get_queue_id():
|
||||
msg = email.message_from_string(
|
||||
"Received: from mail-wr1-x434.google.com (mail-wr1-x434.google.com [IPv6:2a00:1450:4864:20::434])\r\n\t(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits))\r\n\t(No client certificate requested)\r\n\tby mx1.simplelogin.co (Postfix) with ESMTPS id 4FxQmw1DXdz2vK2\r\n\tfor <jglfdjgld@alias.com>; Fri, 4 Jun 2021 14:55:43 +0000 (UTC)"
|
||||
)
|
||||
|
||||
assert get_queue_id(msg) == "4FxQmw1DXdz2vK2"
|
||||
|
||||
|
||||
def test_get_queue_id_from_double_header():
|
||||
msg = load_eml_file("double_queue_id_header.eml")
|
||||
assert get_queue_id(msg) == "6D8C13F069"
|
||||
|
||||
|
||||
def test_should_ignore_bounce(flask_client):
|
||||
assert not should_ignore_bounce("not-exist")
|
||||
|
||||
IgnoreBounceSender.create(mail_from="to-ignore@example.com")
|
||||
assert should_ignore_bounce("to-ignore@example.com")
|
||||
|
||||
|
||||
def test_get_header_unicode():
|
||||
assert get_header_unicode("ab@cd.com") == "ab@cd.com"
|
||||
assert get_header_unicode("=?utf-8?B?w6nDqQ==?=@example.com") == "éé@example.com"
|
||||
|
||||
|
||||
def test_get_orig_message_from_bounce():
|
||||
with open(os.path.join(ROOT_DIR, "local_data", "email_tests", "bounce.eml")) as f:
|
||||
bounce_report = email.message_from_file(f)
|
||||
|
||||
orig_msg = get_orig_message_from_bounce(bounce_report)
|
||||
assert orig_msg["X-SimpleLogin-Type"] == "Forward"
|
||||
assert orig_msg["X-SimpleLogin-Envelope-From"] == "sender@gmail.com"
|
||||
|
||||
|
||||
def test_get_mailbox_bounce_info():
|
||||
with open(os.path.join(ROOT_DIR, "local_data", "email_tests", "bounce.eml")) as f:
|
||||
bounce_report = email.message_from_file(f)
|
||||
|
||||
orig_msg = get_mailbox_bounce_info(bounce_report)
|
||||
assert orig_msg["Final-Recipient"] == "rfc822; not-existing@gmail.com"
|
||||
assert orig_msg["Original-Recipient"] == "rfc822;not-existing@gmail.com"
|
||||
|
||||
|
||||
def test_is_invalid_mailbox_domain(flask_client):
|
||||
domain = random_domain()
|
||||
InvalidMailboxDomain.create(domain=domain, commit=True)
|
||||
|
||||
assert is_invalid_mailbox_domain(domain)
|
||||
assert is_invalid_mailbox_domain(f"sub.{domain}")
|
||||
assert is_invalid_mailbox_domain(f"sub1.sub2.{domain}")
|
||||
|
||||
assert not is_invalid_mailbox_domain("xy.zt")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("object_id", [10**i for i in range(0, 5)])
|
||||
def test_generate_verp_email(object_id):
|
||||
generated_email = generate_verp_email(
|
||||
VerpType.bounce_forward, object_id, "somewhere.net"
|
||||
)
|
||||
info = get_verp_info_from_email(generated_email.lower())
|
||||
assert info[0] == VerpType.bounce_forward
|
||||
assert info[1] == object_id
|
||||
|
||||
|
||||
def test_generate_verp_email_forward_reply_phase():
|
||||
"""make sure the verp type is taken into account in verp generation"""
|
||||
for phase in [
|
||||
VerpType.bounce_forward,
|
||||
VerpType.bounce_reply,
|
||||
VerpType.transactional,
|
||||
]:
|
||||
verp = generate_verp_email(phase, 100)
|
||||
verp_info = get_verp_info_from_email(verp)
|
||||
assert verp_info[0] == phase
|
||||
assert verp_info[1] == 100
|
||||
|
||||
|
||||
def test_add_header_multipart_with_invalid_part():
|
||||
msg = load_eml_file("multipart_alternative.eml")
|
||||
parts = msg.get_payload() + ["invalid"]
|
||||
msg.set_payload(parts)
|
||||
msg = add_header(msg, "INJECT", "INJECT")
|
||||
for i, part in enumerate(msg.get_payload()):
|
||||
if i < 2:
|
||||
assert part.get_payload().index("INJECT") > -1
|
||||
else:
|
||||
assert part == "invalid"
|
||||
|
||||
|
||||
def test_sl_formataddr():
|
||||
# when the name part (first element in the tuple) is empty, formataddr() returns a Header
|
||||
# this makes sure sl_formataddr always returns str
|
||||
assert sl_formataddr(("", "a@b.c")) == "a@b.c"
|
||||
|
||||
assert sl_formataddr(("é", "è@ç.à")) == "=?utf-8?b?w6k=?= <è@ç.à>"
|
||||
# test that the same name-address can't be handled by the built-in formataddr
|
||||
with pytest.raises(UnicodeEncodeError):
|
||||
formataddr(("é", "è@ç.à"))
|
109
app/tests/test_extensions.py
Normal file
109
app/tests/test_extensions.py
Normal file
@ -0,0 +1,109 @@
|
||||
from http import HTTPStatus
|
||||
from random import Random
|
||||
|
||||
from flask import g
|
||||
|
||||
from app import config
|
||||
from app.extensions import limiter
|
||||
from tests.conftest import app as test_app
|
||||
from tests.utils import login
|
||||
|
||||
# IMPORTANT NOTICE
|
||||
# ----------------
|
||||
# This test file has a special behaviour. After each request, a call to fix_rate_limit_after_request must
|
||||
# be performed, in order for the rate_limiting process to work appropriately in test time.
|
||||
# If you want to see why, feel free to refer to the source of the "hack":
|
||||
# https://github.com/alisaifee/flask-limiter/issues/147#issuecomment-642683820
|
||||
|
||||
_ENDPOINT = "/tests/internal/rate_limited"
|
||||
_MAX_PER_MINUTE = 3
|
||||
|
||||
|
||||
@test_app.route(
|
||||
_ENDPOINT,
|
||||
methods=["GET"],
|
||||
)
|
||||
@limiter.limit(f"{_MAX_PER_MINUTE}/minute")
|
||||
def rate_limited_endpoint_1():
|
||||
return "Working", HTTPStatus.OK
|
||||
|
||||
|
||||
def random_ip() -> str:
|
||||
rand = Random()
|
||||
octets = [str(rand.randint(0, 255)) for _ in range(4)]
|
||||
return ".".join(octets)
|
||||
|
||||
|
||||
def fix_rate_limit_after_request():
|
||||
g._rate_limiting_complete = False
|
||||
|
||||
|
||||
def request_headers(source_ip: str) -> dict:
|
||||
return {"X-Forwarded-For": source_ip}
|
||||
|
||||
|
||||
def test_rate_limit_limits_by_source_ip(flask_client):
|
||||
config.DISABLE_RATE_LIMIT = False
|
||||
source_ip = random_ip()
|
||||
|
||||
for _ in range(_MAX_PER_MINUTE):
|
||||
res = flask_client.get(_ENDPOINT, headers=request_headers(source_ip))
|
||||
fix_rate_limit_after_request()
|
||||
assert res.status_code == HTTPStatus.OK
|
||||
|
||||
res = flask_client.get(_ENDPOINT, headers=request_headers(source_ip))
|
||||
fix_rate_limit_after_request()
|
||||
assert res.status_code == HTTPStatus.TOO_MANY_REQUESTS
|
||||
|
||||
# Check that changing the "X-Forwarded-For" allows the request to succeed
|
||||
res = flask_client.get(_ENDPOINT, headers=request_headers(random_ip()))
|
||||
fix_rate_limit_after_request()
|
||||
assert res.status_code == HTTPStatus.OK
|
||||
|
||||
|
||||
def test_rate_limit_limits_by_user_id(flask_client):
|
||||
config.DISABLE_RATE_LIMIT = False
|
||||
# Login with a user
|
||||
login(flask_client)
|
||||
fix_rate_limit_after_request()
|
||||
|
||||
# Run the N requests with a different source IP but with the same user
|
||||
for _ in range(_MAX_PER_MINUTE):
|
||||
res = flask_client.get(_ENDPOINT, headers=request_headers(random_ip()))
|
||||
fix_rate_limit_after_request()
|
||||
assert res.status_code == HTTPStatus.OK
|
||||
|
||||
res = flask_client.get(_ENDPOINT, headers=request_headers(random_ip()))
|
||||
fix_rate_limit_after_request()
|
||||
assert res.status_code == HTTPStatus.TOO_MANY_REQUESTS
|
||||
|
||||
|
||||
def test_rate_limit_limits_by_user_id_ignoring_ip(flask_client):
|
||||
config.DISABLE_RATE_LIMIT = False
|
||||
source_ip = random_ip()
|
||||
|
||||
# Login with a user
|
||||
login(flask_client)
|
||||
fix_rate_limit_after_request()
|
||||
|
||||
# Run the N requests with a different source IP but with the same user
|
||||
for _ in range(_MAX_PER_MINUTE):
|
||||
res = flask_client.get(_ENDPOINT, headers=request_headers(source_ip))
|
||||
fix_rate_limit_after_request()
|
||||
assert res.status_code == HTTPStatus.OK
|
||||
|
||||
res = flask_client.get(_ENDPOINT)
|
||||
fix_rate_limit_after_request()
|
||||
assert res.status_code == HTTPStatus.TOO_MANY_REQUESTS
|
||||
|
||||
# Log out
|
||||
flask_client.cookie_jar.clear()
|
||||
|
||||
# Log in with another user
|
||||
login(flask_client)
|
||||
fix_rate_limit_after_request()
|
||||
|
||||
# Run the request again, reusing the same IP as before
|
||||
res = flask_client.get(_ENDPOINT, headers=request_headers(source_ip))
|
||||
fix_rate_limit_after_request()
|
||||
assert res.status_code == HTTPStatus.OK
|
47
app/tests/test_image_validation.py
Normal file
47
app/tests/test_image_validation.py
Normal file
@ -0,0 +1,47 @@
|
||||
from app.image_validation import ImageFormat, detect_image_format
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_path_to_static_dir() -> Path:
|
||||
this_path = Path(__file__)
|
||||
repo_root_path = this_path.parent.parent
|
||||
return repo_root_path.joinpath("static")
|
||||
|
||||
|
||||
def read_static_file_contents(filename: str) -> bytes:
|
||||
image_path = get_path_to_static_dir().joinpath(filename)
|
||||
with open(image_path.as_posix(), "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def read_test_data_file_contents(filename: str) -> bytes:
|
||||
this_path = Path(__file__)
|
||||
test_data_path = this_path.parent.joinpath("data")
|
||||
file_path = test_data_path.joinpath(filename)
|
||||
with open(file_path.as_posix(), "rb") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def test_non_image_file_returns_unknown():
|
||||
contents = read_static_file_contents("local-storage-polyfill.js")
|
||||
assert detect_image_format(contents) is ImageFormat.Unknown
|
||||
|
||||
|
||||
def test_png_file_is_detected():
|
||||
contents = read_static_file_contents("logo.png")
|
||||
assert detect_image_format(contents) is ImageFormat.Png
|
||||
|
||||
|
||||
def test_jpg_file_is_detected():
|
||||
contents = read_test_data_file_contents("1px.jpg")
|
||||
assert detect_image_format(contents) is ImageFormat.Jpg
|
||||
|
||||
|
||||
def test_webp_file_is_detected():
|
||||
contents = read_test_data_file_contents("1px.webp")
|
||||
assert detect_image_format(contents) is ImageFormat.Webp
|
||||
|
||||
|
||||
def test_svg_file_is_not_detected():
|
||||
contents = read_static_file_contents("icon.svg")
|
||||
assert detect_image_format(contents) is ImageFormat.Unknown
|
26
app/tests/test_jose_utils.py
Normal file
26
app/tests/test_jose_utils.py
Normal file
@ -0,0 +1,26 @@
|
||||
from app.db import Session
|
||||
from app.jose_utils import make_id_token, verify_id_token
|
||||
from app.models import ClientUser, Client
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def test_encode_decode(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
client1 = Client.create_new(name="Demo", user_id=user.id)
|
||||
client1.oauth_client_id = "client-id"
|
||||
client1.oauth_client_secret = "client-secret"
|
||||
Session.commit()
|
||||
|
||||
client_user = ClientUser.create(client_id=client1.id, user_id=user.id)
|
||||
Session.commit()
|
||||
|
||||
jwt_token = make_id_token(client_user)
|
||||
|
||||
assert type(jwt_token) is str
|
||||
assert verify_id_token(jwt_token)
|
||||
|
||||
|
||||
def test_db_tear_down(flask_client):
|
||||
"""make sure the db is reset after each test"""
|
||||
assert len(ClientUser.filter_by().all()) == 0
|
191
app/tests/test_mail_sender.py
Normal file
191
app/tests/test_mail_sender.py
Normal file
@ -0,0 +1,191 @@
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import socket
|
||||
from email.message import Message
|
||||
from random import random
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
from aiosmtpd.controller import Controller
|
||||
|
||||
from app.email import headers
|
||||
from app.mail_sender import (
|
||||
mail_sender,
|
||||
SendRequest,
|
||||
load_unsent_mails_from_fs_and_resend,
|
||||
)
|
||||
from app import config
|
||||
|
||||
|
||||
def create_dummy_send_request() -> SendRequest:
|
||||
to_addr = f"to-{int(random())}@destination.com"
|
||||
from_addr = f"from-{int(random())}@source.com"
|
||||
msg = Message()
|
||||
msg[headers.TO] = to_addr
|
||||
msg[headers.FROM] = from_addr
|
||||
msg[headers.SUBJECT] = f"Random subject {random()}"
|
||||
msg.set_payload(f"Test content {random()}")
|
||||
|
||||
return SendRequest(
|
||||
f"from-{int(random())}@envelope.com",
|
||||
to_addr,
|
||||
msg,
|
||||
)
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_mail_sender_save_to_mem():
|
||||
send_request = create_dummy_send_request()
|
||||
mail_sender.send(send_request, 0)
|
||||
stored_emails = mail_sender.get_stored_emails()
|
||||
assert len(stored_emails) == 1
|
||||
assert stored_emails[0] == send_request
|
||||
|
||||
|
||||
def close_on_connect_dummy_server() -> int:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("localhost", 0))
|
||||
sock.listen()
|
||||
port = sock.getsockname()[1]
|
||||
|
||||
def close_on_accept():
|
||||
connection, _ = sock.accept()
|
||||
connection.close()
|
||||
sock.close()
|
||||
|
||||
threading.Thread(target=close_on_accept, daemon=True).start()
|
||||
return port
|
||||
|
||||
|
||||
def closed_dummy_server() -> int:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
sock.bind(("localhost", 0))
|
||||
sock.listen()
|
||||
port = sock.getsockname()[1]
|
||||
sock.close()
|
||||
return port
|
||||
|
||||
|
||||
def smtp_response_server(smtp_response: str) -> Callable[[], int]:
|
||||
def inner():
|
||||
empty_port = closed_dummy_server()
|
||||
|
||||
class ResponseHandler:
|
||||
async def handle_DATA(self, server, session, envelope) -> str:
|
||||
return smtp_response
|
||||
|
||||
controller = Controller(
|
||||
ResponseHandler(), hostname="localhost", port=empty_port
|
||||
)
|
||||
controller.start()
|
||||
return controller.server.sockets[0].getsockname()[1]
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def compare_send_requests(expected: SendRequest, request: SendRequest):
|
||||
assert request.mail_options == expected.mail_options
|
||||
assert request.rcpt_options == expected.rcpt_options
|
||||
assert request.envelope_to == expected.envelope_to
|
||||
assert request.envelope_from == expected.envelope_from
|
||||
assert request.msg[headers.TO] == expected.msg[headers.TO]
|
||||
assert request.msg[headers.FROM] == expected.msg[headers.FROM]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"server_fn",
|
||||
[
|
||||
close_on_connect_dummy_server,
|
||||
closed_dummy_server,
|
||||
smtp_response_server("421 Retry"),
|
||||
smtp_response_server("500 error"),
|
||||
],
|
||||
)
|
||||
def test_mail_sender_save_unsent_to_disk(server_fn):
|
||||
original_postfix_server = config.POSTFIX_SERVER
|
||||
config.POSTFIX_SERVER = "localhost"
|
||||
config.NOT_SEND_EMAIL = False
|
||||
config.POSTFIX_SUBMISSION_TLS = False
|
||||
config.POSTFIX_PORT = server_fn()
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
config.SAVE_UNSENT_DIR = temp_dir
|
||||
send_request = create_dummy_send_request()
|
||||
assert not mail_sender.send(send_request, 0)
|
||||
found_files = os.listdir(temp_dir)
|
||||
assert len(found_files) == 1
|
||||
loaded_send_request = SendRequest.load_from_file(
|
||||
os.path.join(temp_dir, found_files[0])
|
||||
)
|
||||
compare_send_requests(loaded_send_request, send_request)
|
||||
finally:
|
||||
config.POSTFIX_SERVER = original_postfix_server
|
||||
config.NOT_SEND_EMAIL = True
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_send_unsent_email_from_fs():
|
||||
original_postfix_server = config.POSTFIX_SERVER
|
||||
config.POSTFIX_SERVER = "localhost"
|
||||
config.NOT_SEND_EMAIL = False
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
try:
|
||||
config.SAVE_UNSENT_DIR = temp_dir
|
||||
send_request = create_dummy_send_request()
|
||||
assert not mail_sender.send(send_request, 1)
|
||||
finally:
|
||||
config.POSTFIX_SERVER = original_postfix_server
|
||||
config.NOT_SEND_EMAIL = True
|
||||
saved_files = os.listdir(config.SAVE_UNSENT_DIR)
|
||||
assert len(saved_files) == 1
|
||||
mail_sender.purge_stored_emails()
|
||||
load_unsent_mails_from_fs_and_resend()
|
||||
sent_emails = mail_sender.get_stored_emails()
|
||||
assert len(sent_emails) == 1
|
||||
compare_send_requests(send_request, sent_emails[0])
|
||||
assert sent_emails[0].ignore_smtp_errors
|
||||
assert not os.path.exists(os.path.join(config.SAVE_UNSENT_DIR, saved_files[0]))
|
||||
saved_files = os.listdir(config.SAVE_UNSENT_DIR)
|
||||
assert len(saved_files) == 0
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_failed_resend_does_not_delete_file():
|
||||
original_postfix_server = config.POSTFIX_SERVER
|
||||
config.POSTFIX_SERVER = "localhost"
|
||||
config.NOT_SEND_EMAIL = False
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
config.SAVE_UNSENT_DIR = temp_dir
|
||||
send_request = create_dummy_send_request()
|
||||
# Send and store email in disk
|
||||
assert not mail_sender.send(send_request, 1)
|
||||
saved_files = os.listdir(config.SAVE_UNSENT_DIR)
|
||||
assert len(saved_files) == 1
|
||||
mail_sender.purge_stored_emails()
|
||||
# Send and keep email in disk
|
||||
load_unsent_mails_from_fs_and_resend()
|
||||
sent_emails = mail_sender.get_stored_emails()
|
||||
assert len(sent_emails) == 1
|
||||
compare_send_requests(send_request, sent_emails[0])
|
||||
assert sent_emails[0].ignore_smtp_errors
|
||||
assert os.path.exists(os.path.join(config.SAVE_UNSENT_DIR, saved_files[0]))
|
||||
# No more emails are stored in disk
|
||||
assert saved_files == os.listdir(config.SAVE_UNSENT_DIR)
|
||||
finally:
|
||||
config.POSTFIX_SERVER = original_postfix_server
|
||||
config.NOT_SEND_EMAIL = True
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_ok_mail_does_not_generate_unsent_file():
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
config.SAVE_UNSENT_DIR = temp_dir
|
||||
send_request = create_dummy_send_request()
|
||||
# Send and store email in disk
|
||||
assert mail_sender.send(send_request, 1)
|
||||
saved_files = os.listdir(config.SAVE_UNSENT_DIR)
|
||||
assert len(saved_files) == 0
|
35
app/tests/test_message_utils.py
Normal file
35
app/tests/test_message_utils.py
Normal file
@ -0,0 +1,35 @@
|
||||
import email
|
||||
from app.email_utils import (
|
||||
copy,
|
||||
)
|
||||
from app.message_utils import message_to_bytes
|
||||
|
||||
|
||||
def test_copy():
|
||||
email_str = """
|
||||
From: abcd@gmail.com
|
||||
To: hey@example.org
|
||||
Subject: subject
|
||||
|
||||
Body
|
||||
"""
|
||||
msg = email.message_from_string(email_str)
|
||||
msg2 = copy(msg)
|
||||
assert message_to_bytes(msg) == message_to_bytes(msg2)
|
||||
|
||||
msg = email.message_from_string("👌")
|
||||
msg2 = copy(msg)
|
||||
assert message_to_bytes(msg) == message_to_bytes(msg2)
|
||||
|
||||
|
||||
def test_to_bytes():
|
||||
msg = email.message_from_string("☕️ emoji")
|
||||
assert message_to_bytes(msg)
|
||||
# \n is appended when message is converted to bytes
|
||||
assert message_to_bytes(msg).decode() == "\n☕️ emoji"
|
||||
|
||||
msg = email.message_from_string("ascii")
|
||||
assert message_to_bytes(msg) == b"\nascii"
|
||||
|
||||
msg = email.message_from_string("éèà€")
|
||||
assert message_to_bytes(msg).decode() == "\néèà€"
|
317
app/tests/test_models.py
Normal file
317
app/tests/test_models.py
Normal file
@ -0,0 +1,317 @@
|
||||
import random
|
||||
from uuid import UUID
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
|
||||
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN, NOREPLY
|
||||
from app.db import Session
|
||||
from app.email_utils import parse_full_address, generate_reply_email
|
||||
from app.models import (
|
||||
generate_email,
|
||||
Alias,
|
||||
Contact,
|
||||
Mailbox,
|
||||
SenderFormatEnum,
|
||||
EnumE,
|
||||
Subscription,
|
||||
PlanEnum,
|
||||
PADDLE_SUBSCRIPTION_GRACE_DAYS,
|
||||
)
|
||||
from tests.utils import login, create_new_user, random_token
|
||||
|
||||
|
||||
def test_generate_email(flask_client):
|
||||
email = generate_email()
|
||||
assert email.endswith("@" + EMAIL_DOMAIN)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
UUID(email.split("@")[0], version=4)
|
||||
|
||||
email_uuid = generate_email(scheme=2)
|
||||
assert UUID(email_uuid.split("@")[0], version=4)
|
||||
|
||||
|
||||
def test_profile_picture_url(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
assert user.profile_picture_url() == "http://sl.test/static/default-avatar.png"
|
||||
|
||||
|
||||
def test_suggested_emails_for_user_who_cannot_create_new_alias(flask_client):
|
||||
# make sure user is not in trial
|
||||
user = create_new_user()
|
||||
user.trial_end = None
|
||||
|
||||
# make sure user runs out of quota to create new email
|
||||
for _ in range(MAX_NB_EMAIL_FREE_PLAN):
|
||||
Alias.create_new(user=user, prefix="test")
|
||||
Session.commit()
|
||||
|
||||
suggested_email, other_emails = user.suggested_emails(website_name="test")
|
||||
|
||||
# the suggested email is chosen from existing Alias
|
||||
assert Alias.get_by(email=suggested_email)
|
||||
|
||||
# all other emails are generated emails
|
||||
for email in other_emails:
|
||||
assert Alias.get_by(email=email)
|
||||
|
||||
|
||||
def test_alias_create_random(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
assert alias.email.endswith(EMAIL_DOMAIN)
|
||||
|
||||
|
||||
def test_website_send_to(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
alias = Alias.create_new_random(user)
|
||||
Session.commit()
|
||||
|
||||
prefix = random_token()
|
||||
|
||||
# non-empty name
|
||||
c1 = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"{prefix}@example.com",
|
||||
reply_email="rep@SL",
|
||||
name="First Last",
|
||||
)
|
||||
assert c1.website_send_to() == f'"First Last | {prefix} at example.com" <rep@SL>'
|
||||
|
||||
# empty name, ascii website_from, easy case
|
||||
c1.name = None
|
||||
c1.website_from = f"First Last <{prefix}@example.com>"
|
||||
assert c1.website_send_to() == f'"First Last | {prefix} at example.com" <rep@SL>'
|
||||
|
||||
# empty name, RFC 2047 website_from
|
||||
c1.name = None
|
||||
c1.website_from = f"=?UTF-8?B?TmjGoW4gTmd1eeG7hW4=?= <{prefix}@example.com>"
|
||||
assert c1.website_send_to() == f'"Nhơn Nguyễn | {prefix} at example.com" <rep@SL>'
|
||||
|
||||
|
||||
def test_new_addr_default_sender_format(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.first()
|
||||
prefix = random_token()
|
||||
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"{prefix}@example.com",
|
||||
reply_email="rep@SL",
|
||||
name="First Last",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert contact.new_addr() == f'"First Last - {prefix} at example.com" <rep@SL>'
|
||||
|
||||
# Make sure email isn't duplicated if sender name equals email
|
||||
contact.name = f"{prefix}@example.com"
|
||||
assert contact.new_addr() == f'"{prefix} at example.com" <rep@SL>'
|
||||
|
||||
|
||||
def test_new_addr_a_sender_format(flask_client):
|
||||
user = login(flask_client)
|
||||
user.sender_format = SenderFormatEnum.A.value
|
||||
Session.commit()
|
||||
alias = Alias.first()
|
||||
prefix = random_token()
|
||||
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"{prefix}@example.com",
|
||||
reply_email="rep@SL",
|
||||
name="First Last",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert contact.new_addr() == f'"First Last - {prefix}(a)example.com" <rep@SL>'
|
||||
|
||||
|
||||
def test_new_addr_no_name_sender_format(flask_client):
|
||||
user = login(flask_client)
|
||||
user.sender_format = SenderFormatEnum.NO_NAME.value
|
||||
Session.commit()
|
||||
alias = Alias.first()
|
||||
prefix = random_token()
|
||||
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"{prefix}@example.com",
|
||||
reply_email="rep@SL",
|
||||
name="First Last",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert contact.new_addr() == "rep@SL"
|
||||
|
||||
|
||||
def test_new_addr_name_only_sender_format(flask_client):
|
||||
user = login(flask_client)
|
||||
user.sender_format = SenderFormatEnum.NAME_ONLY.value
|
||||
Session.commit()
|
||||
alias = Alias.first()
|
||||
prefix = random_token()
|
||||
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"{prefix}@example.com",
|
||||
reply_email="rep@SL",
|
||||
name="First Last",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert contact.new_addr() == "First Last <rep@SL>"
|
||||
|
||||
|
||||
def test_new_addr_at_only_sender_format(flask_client):
|
||||
user = login(flask_client)
|
||||
user.sender_format = SenderFormatEnum.AT_ONLY.value
|
||||
Session.commit()
|
||||
alias = Alias.first()
|
||||
prefix = random_token()
|
||||
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"{prefix}@example.com",
|
||||
reply_email="rep@SL",
|
||||
name="First Last",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert contact.new_addr() == f'"{prefix} at example.com" <rep@SL>'
|
||||
|
||||
|
||||
def test_new_addr_unicode(flask_client):
|
||||
user = login(flask_client)
|
||||
alias = Alias.first()
|
||||
|
||||
random_prefix = random_token()
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"{random_prefix}@example.com",
|
||||
reply_email="rep@SL",
|
||||
name="Nhơn Nguyễn",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert (
|
||||
contact.new_addr()
|
||||
== f"=?utf-8?q?Nh=C6=A1n_Nguy=E1=BB=85n_-_{random_prefix}_at_example=2Ecom?= <rep@SL>"
|
||||
)
|
||||
|
||||
# sanity check
|
||||
assert parse_full_address(contact.new_addr()) == (
|
||||
f"Nhơn Nguyễn - {random_prefix} at example.com",
|
||||
"rep@sl",
|
||||
)
|
||||
|
||||
|
||||
def test_mailbox_delete(flask_client):
|
||||
user = create_new_user()
|
||||
|
||||
m1 = Mailbox.create(
|
||||
user_id=user.id, email="m1@example.com", verified=True, commit=True
|
||||
)
|
||||
m2 = Mailbox.create(
|
||||
user_id=user.id, email="m2@example.com", verified=True, commit=True
|
||||
)
|
||||
m3 = Mailbox.create(
|
||||
user_id=user.id, email="m3@example.com", verified=True, commit=True
|
||||
)
|
||||
|
||||
# alias has 2 mailboxes
|
||||
alias = Alias.create_new(user, "prefix", mailbox_id=m1.id)
|
||||
Session.commit()
|
||||
|
||||
alias._mailboxes.append(m2)
|
||||
alias._mailboxes.append(m3)
|
||||
Session.commit()
|
||||
|
||||
assert len(alias.mailboxes) == 3
|
||||
|
||||
# delete m1, should not delete alias
|
||||
Mailbox.delete(m1.id)
|
||||
alias = Alias.get(alias.id)
|
||||
assert len(alias.mailboxes) == 2
|
||||
|
||||
|
||||
def test_EnumE():
|
||||
class E(EnumE):
|
||||
A = 100
|
||||
B = 200
|
||||
|
||||
assert E.has_value(100)
|
||||
assert not E.has_value(101)
|
||||
|
||||
assert E.get_name(100) == "A"
|
||||
assert E.get_name(200) == "B"
|
||||
assert E.get_name(101) is None
|
||||
|
||||
assert E.has_name("A")
|
||||
assert not E.has_name("Not existent")
|
||||
|
||||
assert E.get_value("A") == 100
|
||||
assert E.get_value("Not existent") is None
|
||||
|
||||
|
||||
def test_can_create_new_alias_disabled_user():
|
||||
user = create_new_user()
|
||||
assert user.can_create_new_alias()
|
||||
|
||||
user.disabled = True
|
||||
assert not user.can_create_new_alias()
|
||||
|
||||
|
||||
def test_user_get_subscription_grace_period(flask_client):
|
||||
user = create_new_user()
|
||||
sub = Subscription.create(
|
||||
user_id=user.id,
|
||||
cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234",
|
||||
update_url="https://checkout.paddle.com/subscription/update?user=1234",
|
||||
subscription_id=str(random.random()),
|
||||
event_time=arrow.now(),
|
||||
next_bill_date=arrow.now().shift(days=-PADDLE_SUBSCRIPTION_GRACE_DAYS).date(),
|
||||
plan=PlanEnum.monthly,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert user.get_paddle_subscription() is not None
|
||||
|
||||
sub.next_bill_date = (
|
||||
arrow.now().shift(days=-(PADDLE_SUBSCRIPTION_GRACE_DAYS + 1)).date()
|
||||
)
|
||||
assert user.get_paddle_subscription() is None
|
||||
|
||||
|
||||
def test_create_contact_for_noreply(flask_client):
|
||||
user = create_new_user()
|
||||
alias = Alias.filter(Alias.user_id == user.id).first()
|
||||
|
||||
# create a contact with NOREPLY as reply_email
|
||||
Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=f"{random.random()}@contact.test",
|
||||
reply_email=NOREPLY,
|
||||
commit=True,
|
||||
)
|
||||
|
||||
# create a contact for NOREPLY shouldn't raise CannotCreateContactForReverseAlias
|
||||
contact = Contact.create(
|
||||
user_id=user.id,
|
||||
alias_id=alias.id,
|
||||
website_email=NOREPLY,
|
||||
reply_email=generate_reply_email(NOREPLY, user),
|
||||
)
|
||||
assert contact.website_email == NOREPLY
|
17
app/tests/test_monitoring.py
Normal file
17
app/tests/test_monitoring.py
Normal file
@ -0,0 +1,17 @@
|
||||
from monitoring import _process_ps_output
|
||||
|
||||
|
||||
def test_monitoring_proc_count():
|
||||
data = """
|
||||
PID TTY STAT TIME COMMAND
|
||||
1432 ? S< 0:00 [loop44]
|
||||
3438 ? Ssl 0:00 /bin/sh arg
|
||||
3440 ? Sl 0:00 /bin/cron args
|
||||
3440 ? Sl 0:00 smtp arg
|
||||
3448 ? Sl 0:00 smtpd arg
|
||||
3441 ? Sl 0:00 other smtpd arg
|
||||
"""
|
||||
result = _process_ps_output(["smtp", "smtpd", "cron"], data)
|
||||
assert 1 == result["smtp"]
|
||||
assert 1 == result["smtpd"]
|
||||
assert 0 == result["cron"]
|
79
app/tests/test_oauth_models.py
Normal file
79
app/tests/test_oauth_models.py
Normal file
@ -0,0 +1,79 @@
|
||||
import flask
|
||||
import pytest
|
||||
|
||||
from app.oauth_models import (
|
||||
get_scopes,
|
||||
Scope,
|
||||
get_response_types,
|
||||
ResponseType,
|
||||
response_types_to_str,
|
||||
get_response_types_from_str,
|
||||
)
|
||||
|
||||
|
||||
def test_get_scopes(flask_app):
|
||||
with flask_app.test_request_context("/"):
|
||||
scopes = get_scopes(flask.request)
|
||||
assert scopes == set()
|
||||
|
||||
with flask_app.test_request_context("/?scope=email&scope=name"):
|
||||
scopes = get_scopes(flask.request)
|
||||
assert scopes == {Scope.NAME, Scope.EMAIL}
|
||||
|
||||
# a space between email and name
|
||||
with flask_app.test_request_context("/?scope=email%20name"):
|
||||
scopes = get_scopes(flask.request)
|
||||
assert scopes == {Scope.NAME, Scope.EMAIL}
|
||||
|
||||
# a comma between email and name
|
||||
with flask_app.test_request_context("/?scope=email,name"):
|
||||
scopes = get_scopes(flask.request)
|
||||
assert scopes == {Scope.NAME, Scope.EMAIL}
|
||||
|
||||
# non-existent scope: raise ValueError
|
||||
with flask_app.test_request_context("/?scope=abcd"):
|
||||
with pytest.raises(ValueError):
|
||||
get_scopes(flask.request)
|
||||
|
||||
|
||||
def test_get_response_types(flask_app):
|
||||
with flask_app.test_request_context("/"):
|
||||
response_types = get_response_types(flask.request)
|
||||
assert response_types == set()
|
||||
|
||||
with flask_app.test_request_context("/?response_type=token&response_type=id_token"):
|
||||
response_types = get_response_types(flask.request)
|
||||
assert response_types == {ResponseType.TOKEN, ResponseType.ID_TOKEN}
|
||||
|
||||
# a space as separator
|
||||
with flask_app.test_request_context("/?response_type=token%20id_token"):
|
||||
response_types = get_response_types(flask.request)
|
||||
assert response_types == {ResponseType.TOKEN, ResponseType.ID_TOKEN}
|
||||
|
||||
# a comma as separator
|
||||
with flask_app.test_request_context("/?response_type=id_token,token"):
|
||||
response_types = get_response_types(flask.request)
|
||||
assert response_types == {ResponseType.TOKEN, ResponseType.ID_TOKEN}
|
||||
|
||||
# non-existent response_type: raise ValueError
|
||||
with flask_app.test_request_context("/?response_type=abcd"):
|
||||
with pytest.raises(ValueError):
|
||||
get_response_types(flask.request)
|
||||
|
||||
|
||||
def test_response_types_to_str():
|
||||
assert response_types_to_str([]) == ""
|
||||
assert response_types_to_str([ResponseType.CODE]) == "code"
|
||||
assert (
|
||||
response_types_to_str([ResponseType.CODE, ResponseType.ID_TOKEN])
|
||||
== "code,id_token"
|
||||
)
|
||||
|
||||
|
||||
def test_get_response_types_from_str():
|
||||
assert get_response_types_from_str("") == set()
|
||||
assert get_response_types_from_str("token") == {ResponseType.TOKEN}
|
||||
assert get_response_types_from_str("token id_token") == {
|
||||
ResponseType.TOKEN,
|
||||
ResponseType.ID_TOKEN,
|
||||
}
|
15
app/tests/test_onboarding.py
Normal file
15
app/tests/test_onboarding.py
Normal file
@ -0,0 +1,15 @@
|
||||
from http import HTTPStatus
|
||||
from app.onboarding.utils import CHROME_EXTENSION_LINK
|
||||
|
||||
|
||||
CHROME_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36"
|
||||
|
||||
|
||||
def test_extension_redirect_is_working(flask_client):
|
||||
res = flask_client.get(
|
||||
"/onboarding/extension_redirect", headers={"User-Agent": CHROME_USER_AGENT}
|
||||
)
|
||||
assert res.status_code == HTTPStatus.FOUND
|
||||
|
||||
location_header = res.headers.get("Location")
|
||||
assert location_header == CHROME_EXTENSION_LINK
|
33
app/tests/test_paddle_callback.py
Normal file
33
app/tests/test_paddle_callback.py
Normal file
@ -0,0 +1,33 @@
|
||||
import arrow
|
||||
|
||||
from app import paddle_callback
|
||||
from app.db import Session
|
||||
from app.mail_sender import mail_sender
|
||||
from app.models import Subscription, PlanEnum
|
||||
from tests.utils import create_new_user, random_token
|
||||
|
||||
|
||||
@mail_sender.store_emails_test_decorator
|
||||
def test_failed_payments():
|
||||
user = create_new_user()
|
||||
paddle_sub_id = random_token()
|
||||
sub = Subscription.create(
|
||||
user_id=user.id,
|
||||
cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234",
|
||||
update_url="https://checkout.paddle.com/subscription/update?user=1234",
|
||||
subscription_id=paddle_sub_id,
|
||||
event_time=arrow.now(),
|
||||
next_bill_date=arrow.now().shift(days=10).date(),
|
||||
plan=PlanEnum.monthly,
|
||||
commit=True,
|
||||
)
|
||||
Session.commit()
|
||||
|
||||
paddle_callback.failed_payment(sub, paddle_sub_id)
|
||||
|
||||
sub = Subscription.get_by(subscription_id=paddle_sub_id)
|
||||
assert sub.cancelled
|
||||
|
||||
assert 1 == len(mail_sender.get_stored_emails())
|
||||
mail_sent = mail_sender.get_stored_emails()[0]
|
||||
assert mail_sent.envelope_to == user.email
|
45
app/tests/test_paddle_utils.py
Normal file
45
app/tests/test_paddle_utils.py
Normal file
@ -0,0 +1,45 @@
|
||||
from app.paddle_utils import verify_incoming_request
|
||||
|
||||
|
||||
def test_verify_incoming_request():
|
||||
# the request comes from Paddle simulation
|
||||
request_data = {
|
||||
"alert_id": "1647146853",
|
||||
"alert_name": "payment_succeeded",
|
||||
"balance_currency": "EUR",
|
||||
"balance_earnings": "966.81",
|
||||
"balance_fee": "16.03",
|
||||
"balance_gross": "107.37",
|
||||
"balance_tax": "670.85",
|
||||
"checkout_id": "8-a367127c071e8a2-cba0a50da3",
|
||||
"country": "AU",
|
||||
"coupon": "Coupon 7",
|
||||
"currency": "USD",
|
||||
"customer_name": "customer_name",
|
||||
"earnings": "820.91",
|
||||
"email": "awyman@example.org",
|
||||
"event_time": "2019-12-14 18:43:09",
|
||||
"fee": "0.26",
|
||||
"ip": "65.220.94.158",
|
||||
"marketing_consent": "1",
|
||||
"order_id": "8",
|
||||
"passthrough": "Example String",
|
||||
"payment_method": "paypal",
|
||||
"payment_tax": "0.18",
|
||||
"product_id": "3",
|
||||
"product_name": "Example String",
|
||||
"quantity": "29",
|
||||
"receipt_url": "https://my.paddle.com/receipt/4/5854e29100fd226-440fa7ba7a",
|
||||
"sale_gross": "568.82",
|
||||
"used_price_override": "true",
|
||||
"p_signature": "CQrBWKnAuhBOWdgu6+upbgpLo38c2oQJVgNHLTNsQoaUHtJgHUXzfUfQdcnD9q3EWZuQtyFXXPkygxx/fMbcu+UTnfxkjyecoHio8w4T858jU4VOy1RPqYy6fqazG1vlngiuYqEdgo8OHT/6oIJAf+NWm1v1iwbpr62rDygzJWZrqTzVSKkESfW8/4goxlN2BWr6eaN/4nKQ4gaHq5ee3/7vMmkrLAQG509x9SK3H0bYvh3pvbWMUhYNz8j+7GZRlXcSCpMKw1nkO/jK4IXKW0rtSwgyVjJhpX+/rt2byaCmWEvP0LtGhrug9xAqMYJ3tDCJmwSk2cXG8rPE7oeBwEEElZrQJdbV+i6Tw5rw9LaqEGrjhSkOapfpINdct5UpKXybIyiRZZ111yhJL081T1rtBqb8L+wsPnHG8GzI1Fg5je98j5aXGQU9hcw5nQN779IJQWNN+GbDQZ+Eleu5c6ZYauxpKzE8s/Vs2a4/70KB6WBK6NKxNSIIoOTumKqnfEiPN0pxZp5MMi2dRW7wu7VqvcLbIEYtCkOLnjxVyko32B6AMIgn8CuHvQp9ScPdNdU6B8dBXhdVfV75iYSwx+ythun5d3f357IecaZep27QQmKR/b7/pv4iMOiHKmFQRz9EKwqQm/3Xg2WS4GA4t1X0nslXMuEeRnX6xTaxbvk=",
|
||||
}
|
||||
assert verify_incoming_request(request_data)
|
||||
|
||||
# add a new field in request_data -> verify should fail
|
||||
request_data["new_field"] = "new_field"
|
||||
assert not verify_incoming_request(request_data)
|
||||
|
||||
# modify existing field -> verify should fail
|
||||
request_data["sale_gross"] = "1.23"
|
||||
assert not verify_incoming_request(request_data)
|
65
app/tests/test_pgp_utils.py
Normal file
65
app/tests/test_pgp_utils.py
Normal file
@ -0,0 +1,65 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
import pgpy
|
||||
from pgpy import PGPMessage
|
||||
|
||||
from app.config import ROOT_DIR
|
||||
from app.pgp_utils import (
|
||||
load_public_key,
|
||||
gpg,
|
||||
encrypt_file,
|
||||
encrypt_file_with_pgpy,
|
||||
sign_data,
|
||||
sign_data_with_pgpy,
|
||||
)
|
||||
|
||||
|
||||
def test_load_public_key():
|
||||
public_key_path = os.path.join(ROOT_DIR, "local_data/public-pgp.asc")
|
||||
public_key = open(public_key_path).read()
|
||||
load_public_key(public_key)
|
||||
assert len(gpg.list_keys()) == 1
|
||||
|
||||
|
||||
def test_encrypt():
|
||||
public_key_path = os.path.join(ROOT_DIR, "local_data/public-pgp.asc")
|
||||
public_key = open(public_key_path).read()
|
||||
fingerprint = load_public_key(public_key)
|
||||
secret = encrypt_file(BytesIO(b"abcd"), fingerprint)
|
||||
assert secret != ""
|
||||
|
||||
|
||||
def test_encrypt_file_with_pgpy():
|
||||
encrypt_decrypt_text("heyhey")
|
||||
encrypt_decrypt_text("👍💪")
|
||||
encrypt_decrypt_text("éèù")
|
||||
encrypt_decrypt_text("片仮名")
|
||||
|
||||
|
||||
def encrypt_decrypt_text(text: str):
|
||||
public_key_path = os.path.join(ROOT_DIR, "local_data/public-pgp.asc")
|
||||
public_key = open(public_key_path).read()
|
||||
|
||||
encrypted: PGPMessage = encrypt_file_with_pgpy(text.encode(), public_key)
|
||||
|
||||
# decrypt
|
||||
private_key_path = os.path.join(ROOT_DIR, "local_data/private-pgp.asc")
|
||||
private_key = open(private_key_path).read()
|
||||
priv = pgpy.PGPKey()
|
||||
priv.parse(private_key)
|
||||
decrypted = priv.decrypt(encrypted).message
|
||||
if type(decrypted) == str:
|
||||
assert decrypted == text
|
||||
elif type(decrypted) == bytearray:
|
||||
assert decrypted.decode() == text
|
||||
|
||||
|
||||
def test_sign_data():
|
||||
assert sign_data("heyhey")
|
||||
assert sign_data(b"bytes")
|
||||
|
||||
|
||||
def test_sign_data_with_pgpy():
|
||||
assert sign_data_with_pgpy("unicode")
|
||||
assert sign_data_with_pgpy(b"bytes")
|
42
app/tests/test_prarallel_limiter.py
Normal file
42
app/tests/test_prarallel_limiter.py
Normal file
@ -0,0 +1,42 @@
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import werkzeug.exceptions
|
||||
from flask_login import login_user
|
||||
|
||||
from app.parallel_limiter import _InnerLock
|
||||
from tests.utils import create_new_user
|
||||
|
||||
|
||||
def test_parallel_limiter(flask_app):
|
||||
user = create_new_user()
|
||||
with flask_app.test_request_context():
|
||||
login_user(user)
|
||||
pl = _InnerLock("test", max_wait_secs=1)
|
||||
for loop_id in range(10):
|
||||
assert pl(lambda x: x)(loop_id) == loop_id
|
||||
|
||||
|
||||
def sleep_thread(pl: _InnerLock, sem: Optional[threading.Semaphore] = None):
|
||||
if sem is not None:
|
||||
sem.release()
|
||||
pl(time.sleep)(1)
|
||||
|
||||
|
||||
def test_too_many_requests(flask_app):
|
||||
user = create_new_user()
|
||||
with flask_app.test_request_context():
|
||||
login_user(user)
|
||||
sem = threading.Semaphore(0)
|
||||
pl = _InnerLock("test", max_wait_secs=5)
|
||||
t = threading.Thread(target=sleep_thread, args=(pl, sem))
|
||||
t.daemon = True
|
||||
t.start()
|
||||
sem.acquire()
|
||||
try:
|
||||
got_exception = False
|
||||
pl(sleep_thread)(pl)
|
||||
except werkzeug.exceptions.TooManyRequests:
|
||||
got_exception = True
|
||||
assert got_exception
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user