Skip to content

Commit 8e67666

Browse files
committed
test: 💍 add unit test
Closes: #16
1 parent 0a440de commit 8e67666

File tree

10 files changed

+430
-3
lines changed

10 files changed

+430
-3
lines changed

dev-requirement.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
-r requirement.txt
22
isort==5.10.1
3-
yapf==0.31.0
3+
yapf==0.31.0
4+
pytest==6.2.5
5+
ipython==7.16.3

setup.cfg

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ classifiers =
2020
Programming Language :: Python :: 3.9
2121
Programming Language :: Python :: Implementation :: CPython
2222
Environment :: Console
23-
license_file = LICENSE
23+
license_files = LICENSE
2424
description = A simple image converter for Python.
2525
long_description = file: README.md
2626
long_description_content_type = text/markdown
@@ -45,4 +45,11 @@ exclude =
4545

4646
[options.entry_points]
4747
console_scripts =
48-
pyencrypt = pyencrypt.cli:cli
48+
pyencrypt = pyencrypt.cli:cli
49+
50+
[yapf]
51+
based_on_style = pep8
52+
COLUMN_LIMIT = 119
53+
indent_width = 4
54+
coalesce_brackets = True
55+
dedent_closing_brackets = True

tests/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AES_KEY = b'tiovaZzCF/Hlx/8uw0PXCkiqxCXGosP/AKo6Kn1gECw='

tests/test_aes.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from pyencrypt.aes import AESModeOfOperationECB, add_padding, aes_encrypt, aes_decrypt
2+
import pytest
3+
4+
from constants import AES_KEY
5+
6+
PLAIN_1 = b'hello world'
7+
CIPHER_1 = b'\xc5\xa1\xf8\xed\xf7\xa0\x03\xd8\xffu\x01\xac\x93\xcd+\xe1'
8+
PLAIN_PADDING_1 = PLAIN_1 + b'\x05' * 5
9+
10+
PLAIN_2 = '你好 世界!'.encode()
11+
CIPHER_2 = b'\t\xb6R0B\x1fgz\x06x\x9d\xaf\xb4\xe7_\x9f'
12+
PLAIN_PADDING_2 = PLAIN_2 + b'\x02' * 2
13+
14+
PLAIN_3 = b'abcdefghijklmnop'
15+
CIPHER_3 = b'r\x14\xa7\x92\xd6\x1f\x0c\xf4\x10g\x99\t/\xf0z\xfc' + b'&\x80\xdb\x94\xd1\xf7\x9f\xe0Qo\x05\x98\x7f\xe6j\x8c'
16+
17+
class TestAES:
18+
19+
@pytest.mark.parametrize('key, plain, cipher', [
20+
(AES_KEY, PLAIN_1, CIPHER_1),
21+
(AES_KEY, PLAIN_2, CIPHER_2),
22+
(AES_KEY, PLAIN_3, CIPHER_3),
23+
])
24+
def test_aes_encrypt(self, plain, cipher, key):
25+
assert aes_encrypt(plain, key) == cipher
26+
27+
@pytest.mark.parametrize('key, plain, cipher', [
28+
(AES_KEY, PLAIN_1, CIPHER_1),
29+
(AES_KEY, PLAIN_2, CIPHER_2),
30+
(AES_KEY, PLAIN_3, CIPHER_3),
31+
])
32+
def test_aes_decrypt(self, plain, cipher, key):
33+
assert aes_decrypt(cipher, key) == plain
34+
35+
36+
class TestAESModeOfOperationECB:
37+
38+
def setup_class(self):
39+
self.cipher = AESModeOfOperationECB(AES_KEY)
40+
41+
@pytest.mark.parametrize('length', [17, 18, 19, 20])
42+
def test_encrypt_exception(self, length):
43+
with pytest.raises(ValueError) as excinfo:
44+
self.cipher.encrypt(b'a' * length)
45+
assert str(excinfo.value) == 'plain block must be 16 bytes'
46+
47+
@pytest.mark.parametrize('length', [17, 18, 19, 20])
48+
def test_decrypt_exception(self, length):
49+
with pytest.raises(ValueError) as excinfo:
50+
self.cipher.decrypt(b'a' * length)
51+
assert str(excinfo.value) == 'cipher block must be 16 bytes'
52+
53+
@pytest.mark.parametrize('plain, cipher', [
54+
(PLAIN_PADDING_1, CIPHER_1),
55+
(PLAIN_PADDING_2, CIPHER_2),
56+
])
57+
def test_encrypt(self,plain, cipher):
58+
assert self.cipher.encrypt(plain) == cipher
59+
60+
@pytest.mark.parametrize('plain, cipher', [
61+
(PLAIN_PADDING_1, CIPHER_1),
62+
(PLAIN_PADDING_2, CIPHER_2),
63+
])
64+
def test_decrypt(self,plain, cipher):
65+
assert self.cipher.decrypt(cipher) == plain

tests/test_decrypt.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from pyencrypt.decrypt import *
2+
from pyencrypt.encrypt import encrypt_file, encrypt_key
3+
from constants import AES_KEY
4+
import pytest
5+
6+
from pyencrypt.generate import generate_aes_key
7+
8+
9+
@pytest.mark.parametrize('key', [
10+
AES_KEY,
11+
generate_aes_key(),
12+
])
13+
def test_decrypt_key(key):
14+
cipher_key, d, n = encrypt_key(key)
15+
assert decrypt_key(cipher_key, d, n) == key.decode()
16+
17+
18+
@pytest.fixture
19+
def encrypted_python_file_path(tmp_path):
20+
path = tmp_path / 'test.py'
21+
path.touch()
22+
path.write_text('print("hello world")')
23+
new_path = tmp_path / 'test.pye'
24+
encrypt_file(path, AES_KEY, new_path=new_path)
25+
path.unlink()
26+
return new_path
27+
28+
29+
@pytest.mark.parametrize(
30+
'path,key,exception', [
31+
(Path('tests/test.py'), AES_KEY, Exception),
32+
(Path('tests/__init__.pye'), AES_KEY, FileNotFoundError),
33+
]
34+
)
35+
def test_decrypt_file_exception(path, key, exception):
36+
with pytest.raises(exception) as excinfo:
37+
decrypt_file(path, key)
38+
assert excinfo.value.__class__ == exception
39+
40+
41+
def test_decrypt_file_default(encrypted_python_file_path):
42+
assert isinstance(decrypt_file(encrypted_python_file_path, AES_KEY), bytes) == True
43+
assert encrypted_python_file_path.exists() == True
44+
45+
46+
def test_decrypt_file_delete_origin(encrypted_python_file_path):
47+
decrypt_file(encrypted_python_file_path, AES_KEY, delete_origin=True)
48+
assert encrypted_python_file_path.exists() == False
49+
50+
51+
def test_decrypt_file_new_path(encrypted_python_file_path):
52+
new_path = encrypted_python_file_path.parent / 'test.py'
53+
decrypt_file(encrypted_python_file_path, AES_KEY, new_path=new_path)
54+
assert new_path.exists() == True
55+
assert encrypted_python_file_path.exists() == True
56+
57+
58+
def test_decrypt_file_new_path_exception(encrypted_python_file_path):
59+
new_path = encrypted_python_file_path.parent / 'test.pye'
60+
with pytest.raises(Exception) as excinfo:
61+
decrypt_file(encrypted_python_file_path, AES_KEY, new_path=new_path)
62+
assert str(excinfo.value) == 'Origin file path must be py suffix.'

tests/test_encrypt.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import shutil
2+
import pytest
3+
from pyencrypt.encrypt import *
4+
from pyencrypt.decrypt import *
5+
import os
6+
7+
from constants import AES_KEY
8+
from pyencrypt.generate import generate_aes_key
9+
10+
11+
12+
@pytest.mark.parametrize('key', [
13+
AES_KEY,
14+
generate_aes_key(),
15+
])
16+
def test_encrypt_key(key):
17+
cipher, d, n = encrypt_key(key)
18+
assert isinstance(cipher, str)
19+
assert isinstance(d, int)
20+
assert isinstance(n, int)
21+
22+
23+
@pytest.mark.parametrize(
24+
'path,expected', [
25+
(Path('__init__.py'), False),
26+
(Path('pyencrypt/__init__.py'), False),
27+
(Path('management/commands/user.py'), False),
28+
(Path('tests/test.pye'), False),
29+
(Path('tests/test_encrypt.py'), True),
30+
]
31+
)
32+
def test_can_encrypt(path, expected):
33+
assert can_encrypt(path) == expected
34+
35+
36+
class TestGenarateSoFile:
37+
38+
def setup_method(self, method):
39+
if method.__name__ == 'test_generate_so_file_default_path':
40+
shutil.rmtree((Path(os.getcwd()) / 'encrypted').as_posix(), ignore_errors=True)
41+
42+
@pytest.mark.parametrize('key', [
43+
AES_KEY,
44+
generate_aes_key(),
45+
])
46+
def test_generate_so_file(self, key, tmp_path):
47+
cipher_key, d, n = encrypt_key(key)
48+
assert generate_so_file(cipher_key, d, n, tmp_path)
49+
assert (tmp_path / 'encrypted' / 'loader.py').exists() == True
50+
assert (tmp_path / 'encrypted' / 'loader_origin.py').exists() == True
51+
assert list((tmp_path / 'encrypted').glob('loader.cpython-*-*.so')) != []
52+
53+
@pytest.mark.parametrize('key', [
54+
AES_KEY,
55+
generate_aes_key(),
56+
])
57+
def test_generate_so_file_default_path(self, key):
58+
cipher_key, d, n = encrypt_key(key)
59+
assert generate_so_file(cipher_key, d, n)
60+
assert (Path(os.getcwd()) / 'encrypted' / 'loader.py').exists() == True
61+
assert (Path(os.getcwd()) / 'encrypted' / 'loader_origin.py').exists() == True
62+
assert list((Path(os.getcwd()) / 'encrypted').glob('loader.cpython-*-*.so')) != []
63+
64+
65+
@pytest.mark.parametrize(
66+
'path,key,exception',
67+
[
68+
(Path('tests/test.py'), AES_KEY, FileNotFoundError),
69+
(Path('tests/test.pye'), AES_KEY, Exception), # TODO: 封装Exception
70+
(Path('tests/__init__.py'), AES_KEY, Exception),
71+
]
72+
)
73+
def test_encrypt_file_path_exception(path, key, exception):
74+
with pytest.raises(exception) as excinfo:
75+
encrypt_file(path, key)
76+
assert excinfo.value.__class__ == exception
77+
78+
79+
@pytest.fixture
80+
def python_file_path(tmp_path):
81+
fn = tmp_path / 'test.py'
82+
fn.touch()
83+
fn.write_text('print("hello world")')
84+
return fn
85+
86+
87+
def test_encrypt_file_default(python_file_path):
88+
assert isinstance(encrypt_file(python_file_path, AES_KEY), bytes) == True
89+
assert python_file_path.exists() == True
90+
91+
92+
def test_encrypt_file_delete_origin(python_file_path):
93+
encrypt_file(python_file_path, AES_KEY, delete_origin=True)
94+
assert python_file_path.exists() == False
95+
96+
97+
def test_encrypt_file_new_path(python_file_path):
98+
new_path = python_file_path.parent / 'test.pye'
99+
encrypt_file(python_file_path, AES_KEY, new_path=new_path)
100+
assert new_path.exists() == True
101+
assert python_file_path.exists() == True
102+
103+
def test_encrypt_file_new_path_exception(python_file_path):
104+
new_path = python_file_path.parent / 'test.py'
105+
with pytest.raises(Exception) as excinfo:
106+
encrypt_file(python_file_path, AES_KEY, new_path=new_path)
107+
assert str(excinfo.value) == 'Encrypted file path must be pye suffix.'

tests/test_generate.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from random import randint
2+
from pyencrypt.generate import *
3+
import pytest
4+
5+
6+
def test_generate_aes_key_default():
7+
assert isinstance(generate_aes_key(), bytes)
8+
9+
10+
@pytest.mark.parametrize('size', [32, 64, 1024, 4096])
11+
def test_generate_aes_key(size):
12+
assert isinstance(generate_aes_key(size), bytes)
13+
14+
15+
@pytest.mark.parametrize('bits', [1024, 2048, 4096])
16+
def test_generate_rsa_number(bits):
17+
numbers = generate_rsa_number(bits)
18+
assert len(numbers) == 5
19+
p, q, n, e, d = numbers['p'], numbers['q'], numbers['n'], numbers['e'], numbers['d']
20+
assert p * q == n
21+
assert e * d % (p - 1) == 1
22+
assert e * d % (q - 1) == 1
23+
plain = randint(0, n)
24+
assert pow(pow(plain, e, n), d, n) == plain
25+
26+
27+
@pytest.mark.parametrize('bits', [123, 456, 789])
28+
def test_generate_rsa_number_exception(bits):
29+
with pytest.raises(ValueError) as excinfo:
30+
generate_rsa_number(bits)
31+
assert str(excinfo.value) == "RSA modulus length must be a multiple of 256 and >= 1024"

tests/test_loader.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import pytest
2+
from pathlib import Path
3+
import shutil
4+
from pyencrypt.encrypt import *
5+
from constants import AES_KEY
6+
7+
8+
@pytest.fixture(scope='module')
9+
def python_file(tmp_path_factory):
10+
tmp_path = tmp_path_factory.mktemp('file')
11+
path = tmp_path / 'aaa.py'
12+
path.touch()
13+
path.write_text("""\
14+
def main():
15+
return 'hello world'
16+
""")
17+
new_path = path.with_suffix('.pye')
18+
encrypt_file(path, AES_KEY, new_path=new_path)
19+
path.unlink()
20+
cipher_key, d, n = encrypt_key(AES_KEY)
21+
generate_so_file(cipher_key, d, n, tmp_path)
22+
loader_path = list((tmp_path / 'encrypted').glob('loader.cpython-*-*.so'))[0]
23+
shutil.copy(loader_path, tmp_path)
24+
shutil.rmtree(tmp_path / 'encrypted')
25+
return new_path
26+
27+
28+
def test_python_file_with_sys_path(python_file: Path, monkeypatch):
29+
monkeypatch.syspath_prepend(python_file.parent.as_posix())
30+
import loader
31+
from aaa import main
32+
assert main() == 'hello world'
33+
34+
35+
@pytest.fixture(scope='module')
36+
def python_package(tmp_path_factory):
37+
pkg_path = tmp_path_factory.mktemp('package')
38+
path = pkg_path / 'bbb'
39+
path.mkdir()
40+
(path / '__init__.py').touch()
41+
path /= 'ccc.py'
42+
path.touch()
43+
path.write_text("""\
44+
def main():
45+
return 'hello world'
46+
""")
47+
new_path = path.with_suffix('.pye')
48+
encrypt_file(path, AES_KEY, new_path=new_path)
49+
path.unlink()
50+
cipher_key, d, n = encrypt_key(AES_KEY)
51+
generate_so_file(cipher_key, d, n, pkg_path)
52+
loader_path = list((pkg_path / 'encrypted').glob('loader.cpython-*-*.so'))[0]
53+
shutil.copy(loader_path, pkg_path)
54+
shutil.rmtree(pkg_path / 'encrypted')
55+
return pkg_path
56+
57+
58+
def test_python_package(python_package: Path, monkeypatch):
59+
monkeypatch.syspath_prepend(python_package.as_posix())
60+
import loader
61+
from bbb.ccc import main
62+
assert main() == 'hello world'
63+
64+
65+
def test_python_package_without_init_file(tmp_path_factory):
66+
pkg_path = tmp_path_factory.mktemp('package')
67+
path = pkg_path / 'aaa'
68+
path.mkdir()
69+
path /= 'bbb.py'
70+
path.touch()
71+
path.write_text("""\
72+
def main():
73+
return 'hello world'
74+
""")
75+
new_path = path.with_suffix('.pye')
76+
encrypt_file(path, AES_KEY, new_path=new_path)
77+
path.unlink()
78+
cipher_key, d, n = encrypt_key(AES_KEY)
79+
generate_so_file(cipher_key, d, n, pkg_path)
80+
loader_path = list((pkg_path / 'encrypted').glob('loader.cpython-*-*.so'))[0]
81+
shutil.copy(loader_path, pkg_path)
82+
shutil.rmtree(pkg_path / 'encrypted')
83+
import loader
84+
from bbb.ccc import main

0 commit comments

Comments
 (0)