Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Make the ``validators`` package importable without an install.

Tries, in order: the src-layout of a checked-out repo (``../src``),
this workdir's ``../validators-src/src`` clone, and the repo root itself.
Only directories that actually contain the ``validators`` package are added.
"""

import os
import sys

_HERE = os.path.dirname(os.path.abspath(__file__))
_CANDIDATES = (
os.path.join(_HERE, "..", "src"),
os.path.join(_HERE, "..", "validators-src", "src"),
os.path.join(_HERE, ".."),
)
for _candidate in _CANDIDATES:
if os.path.isdir(os.path.join(_candidate, "validators")):
sys.path.insert(0, os.path.normpath(_candidate))
139 changes: 139 additions & 0 deletions tests/test_cards_extended.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Extended coverage for card numbers and the brand validators.

The repo's card tests (219 lines) cover the standard test numbers;
this file pins the brand/prefix/length matrix the existing suite only
samples: every brand against every other brand's number, Luhn failures
that keep a valid prefix, and length-only failures.

All card numbers used here are the well-known gateway test numbers
already used in the repo's own tests and docstrings.
"""

import pytest

from validators import (
ValidationError,
amex,
card_number,
diners,
discover,
jcb,
mastercard,
mir,
unionpay,
visa,
)


# ---------------------------------------------------------------- generic Luhn


@pytest.mark.parametrize(
"value",
["4242424242424242", "378282246310005", "6011111111111117", "30569309025904"],
)
def test_card_number_returns_true_on_luhn_valid_numbers(value):
assert card_number(value) is True


@pytest.mark.parametrize(
"value",
[
"",
"4242424242424241", # last digit breaks Luhn
"4242 4242 4242 4242", # spaces are not stripped
"4242-4242-4242-4242", # dashes are not stripped
"abcd", # non-digits fail int() conversion
"123", # Luhn sum is not a multiple of 10
],
)
def test_card_number_returns_failed_validation_on_invalid_numbers(value):
assert isinstance(card_number(value), ValidationError)


# ---------------------------------------------------------------- brands


@pytest.mark.parametrize(
("validator", "value"),
[
(visa, "4242424242424242"),
(visa, "4111111111111111"),
(mastercard, "5555555555554444"),
(mastercard, "2223003122003222"), # 2-series Mastercard
(amex, "378282246310005"),
(amex, "371449635398431"),
(unionpay, "6200000000000005"),
(diners, "30569309025904"), # 14 digits
(diners, "3056930009020004"),
(diners, "36148900647913"),
(jcb, "3530111333300000"),
(jcb, "3566002020360505"),
(discover, "6011111111111117"),
(discover, "6011000990139424"),
(discover, "6445644564456445"), # 644 prefix
(mir, "2200123456789019"),
],
)
def test_brand_validators_accept_their_own_numbers(validator, value):
assert validator(value) is True


@pytest.mark.parametrize(
("validator", "value"),
[
# Wrong brand, Luhn-valid number: prefix/length must reject it.
(visa, "5555555555554444"),
(mastercard, "4242424242424242"),
(amex, "4242424242424242"),
(unionpay, "4242424242424242"),
(diners, "4242424242424242"),
(jcb, "4242424242424242"),
(discover, "4242424242424242"),
(mir, "4242424242424242"),
# Right prefix family, wrong length.
(visa, "4222222222222"), # 13 digits, not 16
(visa, "424242424242424"), # 15 digits, not 16
(amex, "3782822463100050"), # 16 digits, not 15
(mastercard, "555555555555444"), # 15 digits, not 16
# Right prefix, broken Luhn.
(amex, "378282246310006"),
(unionpay, "6200000000000006"),
(jcb, "3530111333300001"),
(discover, "6011111111111118"),
# Prefix just outside the accepted range.
(mastercard, "2720992713989755"), # 2720-series edge, Luhn-invalid
(mir, "2205123456789019"), # 2205 is outside 2200-2204
(diners, "32148900647913"), # 32 is not a Diners prefix
(discover, "6221111111111117"), # 62 is UnionPay, not Discover
],
)
def test_brand_validators_reject_other_numbers(validator, value):
assert isinstance(validator(value), ValidationError)


def test_brand_validators_reject_empty_and_non_digit_input():
for validator in (visa, mastercard, amex, unionpay, diners, jcb, discover, mir):
assert isinstance(validator(""), ValidationError)
assert isinstance(validator("abcd"), ValidationError)


# ---------------------------------------------------------------- ValidationError mechanics


def test_validation_error_is_falsy_and_carries_function_and_args():
result = visa("5555555555554444")
assert isinstance(result, ValidationError)
assert bool(result) is False
assert result.func.__name__ == "visa"
assert result.value == "5555555555554444"
assert repr(result).startswith("ValidationError(func=visa")


def test_validation_error_raised_with_r_ve():
with pytest.raises(ValidationError):
visa("5555555555554444", r_ve=True)


def test_validation_error_not_raised_with_r_ve_on_valid_input():
assert visa("4242424242424242", r_ve=True) is True
180 changes: 180 additions & 0 deletions tests/test_cron_encoding_extended.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""Extended coverage for cron and the base16/32/58/64 encodings.

Existing cron tests are 59 lines and only exercise the happy path plus a
few out-of-range fields; the step/list grammar below is probed, not
assumed. Encoding tests in the repo are 151 lines but leave padding
and alphabet edge cases unpinned.
"""

import pytest

from validators import (
ValidationError,
base16,
base32,
base58,
base64,
cron,
)


# ---------------------------------------------------------------- cron


@pytest.mark.parametrize(
"value",
[
"0 0 * * *",
"*/15 * * * *", # step over the whole range
"0 9-17 * * 1-5", # ranges in two fields
"5/10 * * * *", # a single value with a step is accepted
"0 0 * * 7", # Sunday may be written as 7
"1,2,3 * * * *", # comma list
"0 0 1,15 * *",
"*/2 0-23/2 * * *", # step over a range
" 0 0 * * * ", # extra whitespace is stripped/split away
"30 4 1 * *",
"59 23 31 12 7", # every field at its maximum
"0 0 1 1 0", # every field at its minimum
],
)
def test_cron_returns_true_on_valid_expressions(value):
assert cron(value) is True


@pytest.mark.parametrize(
"value",
[
"*/0 * * * *", # step must be >= 1
"*/abc * * * *", # step must be decimal
"30-20 * * * *", # reversed range
"1,,2 * * * *", # empty list item
"1- * * * *", # dangling range end
"1-2-3 * * * *", # three-part range
"0 0 * * MON", # month/weekday names are not supported
"0 0 0 * *", # day of month starts at 1
"0 0 * 0 *", # month starts at 1
"0 0 * * 8", # weekday ends at 7
"60 * * * *", # minute ends at 59
"0 24 * * *", # hour ends at 23
"0 0 32 * *", # day of month ends at 31
"0 0 * 13 *", # month ends at 12
"-1 * * * *", # negatives are not decimal
"* * * *", # four fields: badly formatted
"0 0 * * * *", # six fields: badly formatted
"",
],
)
def test_cron_returns_failed_validation_on_invalid_expressions(value):
assert isinstance(cron(value), ValidationError)


def test_cron_wrong_field_count_carries_the_reason():
result = cron("* * * *")
assert isinstance(result, ValidationError)
assert result.reason == "Badly formatted cron string"


# ---------------------------------------------------------------- base16


@pytest.mark.parametrize("value", ["00ff", "FF", "a3f4b2", "00" * 10])
def test_base16_returns_true_on_valid_values(value):
assert base16(value) is True


@pytest.mark.parametrize("value", ["", "0", "abc", "0x12", "a3f4Z1", "ff "])
def test_base16_returns_failed_validation_on_invalid_values(value):
assert isinstance(base16(value), ValidationError)


# ---------------------------------------------------------------- base32


@pytest.mark.parametrize(
"value",
[
"MY======", # 1 byte + padding
"MZXW6===", # 2 bytes + padding
"MZXW6YQ=", # 4 bytes + padding
"MZXW6YTBOI======", # 8-char group + padding
"MZXW6", # padding is optional for the short forms
"MZXW6YQ", # 6 chars, no padding needed
"ABCDEFGHIJKLMNOP", # two full 8-char groups
],
)
def test_base32_returns_true_on_valid_values(value):
assert base32(value) is True


@pytest.mark.parametrize(
"value",
[
"",
"MfZW3zLT9Q======", # lower case is not in the alphabet
"MZXW6Y==", # 6 chars may not take "==" padding
"MZXW6YQ==", # 7 chars may take at most one "="
"MZXW6YQ===", # too much padding
"MZXW68==", # '8' is not in the base32 alphabet
"MZXW6YQ=extra",
],
)
def test_base32_returns_failed_validation_on_invalid_values(value):
assert isinstance(base32(value), ValidationError)


# ---------------------------------------------------------------- base58


@pytest.mark.parametrize("value", ["StV1DL6CwTryKyV", "cUSECm5YzcXJwP", "1111111111"])
def test_base58_returns_true_on_valid_values(value):
assert base58(value) is True


@pytest.mark.parametrize(
"value",
[
"",
"0abc", # '0' is excluded from base58
"Oabc", # 'O' is excluded
"Iabc", # 'I' is excluded
"labc", # 'l' is excluded
"abc def", # spaces are excluded
"abc+def", # '+' is excluded
],
)
def test_base58_returns_failed_validation_on_invalid_values(value):
assert isinstance(base58(value), ValidationError)


# ---------------------------------------------------------------- base64


@pytest.mark.parametrize(
"value",
[
"YQ==", # 1 byte
"YWI=", # 2 bytes
"TWFu", # 3 bytes, no padding
"Y2hhcmFjdGVyIHNldA==",
"TWFuTWFu", # two full groups
],
)
def test_base64_returns_true_on_valid_values(value):
assert base64(value) is True


@pytest.mark.parametrize(
"value",
[
"",
"YQ=", # 1 byte needs "==", not "="
"YWI", # missing padding
"YQ==YQ==", # padding may only appear at the end
"Y W I=", # spaces are not in the alphabet
"YQ==\n",
"YQ*=",
],
)
def test_base64_returns_failed_validation_on_invalid_values(value):
assert isinstance(base64(value), ValidationError)
Loading