|  | 
|  | 1 | +import re | 
|  | 2 | +import string | 
|  | 3 | +import random | 
|  | 4 | +from datetime import timedelta | 
|  | 5 | +from urllib.parse import urlparse, parse_qs | 
|  | 6 | +from starlette.datastructures import URLPath | 
|  | 7 | +from main import app | 
|  | 8 | +from utils.auth import ( | 
|  | 9 | +    create_access_token, | 
|  | 10 | +    create_refresh_token, | 
|  | 11 | +    verify_password, | 
|  | 12 | +    get_password_hash, | 
|  | 13 | +    validate_token, | 
|  | 14 | +    generate_password_reset_url, | 
|  | 15 | +    COMPILED_PASSWORD_PATTERN, | 
|  | 16 | +    convert_python_regex_to_html | 
|  | 17 | +) | 
|  | 18 | + | 
|  | 19 | + | 
|  | 20 | +def test_convert_python_regex_to_html(): | 
|  | 21 | +    PYTHON_SPECIAL_CHARS = r"(?=.*[\[\]\\@$!%*?&{}<>.,'#\-_=+\(\):;|~/\^])" | 
|  | 22 | +    HTML_EQUIVALENT = r"(?=.*[\[\]\\@$!%*?&\{\}\<\>\.\,\\'#\-_=\+\(\):;\|~\/\^])" | 
|  | 23 | + | 
|  | 24 | +    PYTHON_SPECIAL_CHARS = convert_python_regex_to_html(PYTHON_SPECIAL_CHARS) | 
|  | 25 | + | 
|  | 26 | +    assert PYTHON_SPECIAL_CHARS == HTML_EQUIVALENT | 
|  | 27 | + | 
|  | 28 | + | 
|  | 29 | +def test_password_hashing(): | 
|  | 30 | +    password = "Test123!@#" | 
|  | 31 | +    hashed = get_password_hash(password) | 
|  | 32 | +    assert verify_password(password, hashed) | 
|  | 33 | +    assert not verify_password("wrong_password", hashed) | 
|  | 34 | + | 
|  | 35 | + | 
|  | 36 | +def test_token_creation_and_validation(): | 
|  | 37 | +    data = {"sub" : "[email protected]" } | 
|  | 38 | + | 
|  | 39 | +    # Test access token | 
|  | 40 | +    access_token = create_access_token(data) | 
|  | 41 | +    decoded = validate_token(access_token, "access") | 
|  | 42 | +    assert decoded is not None | 
|  | 43 | +    assert decoded["sub"] == data["sub"] | 
|  | 44 | +    assert decoded["type"] == "access" | 
|  | 45 | + | 
|  | 46 | +    # Test refresh token | 
|  | 47 | +    refresh_token = create_refresh_token(data) | 
|  | 48 | +    decoded = validate_token(refresh_token, "refresh") | 
|  | 49 | +    assert decoded is not None | 
|  | 50 | +    assert decoded["sub"] == data["sub"] | 
|  | 51 | +    assert decoded["type"] == "refresh" | 
|  | 52 | + | 
|  | 53 | + | 
|  | 54 | +def test_expired_token(): | 
|  | 55 | +    data = {"sub" : "[email protected]" } | 
|  | 56 | +    expired_delta = timedelta(minutes=-10) | 
|  | 57 | +    expired_token = create_access_token(data, expired_delta) | 
|  | 58 | +    decoded = validate_token(expired_token, "access") | 
|  | 59 | +    assert decoded is None | 
|  | 60 | + | 
|  | 61 | + | 
|  | 62 | +def test_invalid_token_type(): | 
|  | 63 | +    data = {"sub" : "[email protected]" } | 
|  | 64 | +    access_token = create_access_token(data) | 
|  | 65 | +    decoded = validate_token(access_token, "refresh") | 
|  | 66 | +    assert decoded is None | 
|  | 67 | + | 
|  | 68 | +def test_password_reset_url_generation(): | 
|  | 69 | +    """ | 
|  | 70 | +    Tests that the password reset URL is correctly formatted and contains | 
|  | 71 | +    the required query parameters. | 
|  | 72 | +    """ | 
|  | 73 | + | 
|  | 74 | +    test_token = "abc123" | 
|  | 75 | + | 
|  | 76 | +    url = generate_password_reset_url(test_email, test_token) | 
|  | 77 | + | 
|  | 78 | +    # Parse the URL | 
|  | 79 | +    parsed = urlparse(url) | 
|  | 80 | +    query_params = parse_qs(parsed.query) | 
|  | 81 | + | 
|  | 82 | +    # Get the actual path from the FastAPI app | 
|  | 83 | +    reset_password_path: URLPath = app.url_path_for("reset_password") | 
|  | 84 | + | 
|  | 85 | +    # Verify URL path | 
|  | 86 | +    assert parsed.path == str(reset_password_path) | 
|  | 87 | + | 
|  | 88 | +    # Verify query parameters | 
|  | 89 | +    assert "email" in query_params | 
|  | 90 | +    assert "token" in query_params | 
|  | 91 | +    assert query_params["email"][0] == test_email | 
|  | 92 | +    assert query_params["token"][0] == test_token | 
|  | 93 | + | 
|  | 94 | +def test_password_pattern(): | 
|  | 95 | +    """ | 
|  | 96 | +    Tests that the password pattern is correctly defined. to require at least | 
|  | 97 | +    one uppercase letter, one lowercase letter, one digit, and one special | 
|  | 98 | +    character, and at least 8 characters long. Allowed special characters are: | 
|  | 99 | +    !@#$%^&*()_+-=[]{}|;:,.<>? | 
|  | 100 | +    """ | 
|  | 101 | +    special_characters = "!@#$%^&*()_+-=[]{}|;:,.<>?" | 
|  | 102 | +    uppercase_letters = string.ascii_uppercase | 
|  | 103 | +    lowercase_letters = string.ascii_lowercase | 
|  | 104 | +    digits = string.digits | 
|  | 105 | + | 
|  | 106 | +    required_elements = { | 
|  | 107 | +        "special": special_characters, | 
|  | 108 | +        "uppercase": uppercase_letters, | 
|  | 109 | +        "lowercase": lowercase_letters, | 
|  | 110 | +        "digit": digits | 
|  | 111 | +    } | 
|  | 112 | + | 
|  | 113 | +    # Valid password tests | 
|  | 114 | +    for element in required_elements: | 
|  | 115 | +        for c in required_elements[element]: | 
|  | 116 | +            password = c + "test" | 
|  | 117 | +            for other_element in required_elements: | 
|  | 118 | +                if other_element != element: | 
|  | 119 | +                    password += random.choice(required_elements[other_element]) | 
|  | 120 | +            # Randomize the order of the characters in the string | 
|  | 121 | +            password = ''.join(random.sample(password, len(password))) | 
|  | 122 | +        assert re.match(COMPILED_PASSWORD_PATTERN, password) is not None, f"Password {password} does not match the pattern" | 
|  | 123 | + | 
|  | 124 | +    # Invalid password tests | 
|  | 125 | + | 
|  | 126 | +    # Empty password | 
|  | 127 | +    password = "" | 
|  | 128 | +    assert re.match(COMPILED_PASSWORD_PATTERN, password) is None | 
|  | 129 | + | 
|  | 130 | +    # Too short | 
|  | 131 | +    password = "aA1!aA1" | 
|  | 132 | +    assert re.match(COMPILED_PASSWORD_PATTERN, password) is None | 
|  | 133 | + | 
|  | 134 | +    # No uppercase letter | 
|  | 135 | +    password = "a1!" * 3 | 
|  | 136 | +    assert re.match(COMPILED_PASSWORD_PATTERN, password) is None | 
|  | 137 | + | 
|  | 138 | +    # No lowercase letter | 
|  | 139 | +    password = "A1!" * 3 | 
|  | 140 | +    assert re.match(COMPILED_PASSWORD_PATTERN, password) is None | 
|  | 141 | + | 
|  | 142 | +    # No digit | 
|  | 143 | +    password = "aA!" * 3 | 
|  | 144 | +    assert re.match(COMPILED_PASSWORD_PATTERN, password) is None | 
|  | 145 | + | 
|  | 146 | +    # No special character | 
|  | 147 | +    password = "aA1" * 3 | 
|  | 148 | +    assert re.match(COMPILED_PASSWORD_PATTERN, password) is None | 
0 commit comments