Skip to content

Commit 7657214

Browse files
committed
GH-4 # add check_password_leak logic
1 parent bbff95e commit 7657214

File tree

9 files changed

+106
-14
lines changed

9 files changed

+106
-14
lines changed

poetry.lock

Lines changed: 22 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ redis = "^5.2.1"
2626
docopt = "^0.6.2"
2727
xxhash = "^3.5.0"
2828
fastapi = {extras = ["standard"], version = "^0.115.8"}
29+
pydantic-settings = "^2.7.1"
2930

3031
[tool.ruff]
3132
line-length = 120

src/api/dependencies.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from functools import cache
12
from typing import Annotated
23

34
from fastapi.params import Depends
@@ -9,6 +10,11 @@ def get_settings() -> Settings:
910
return Settings()
1011

1112

12-
# TODO cache settings + password_storage ?
13+
@cache
14+
def get_settings_cached() -> Settings:
15+
return Settings()
16+
17+
18+
# TODO cache password_storage ?
1319

14-
SettingsDep = Annotated[Settings, Depends(get_settings)]
20+
SettingsDep = Annotated[Settings, Depends(get_settings_cached)]

src/api/router.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Annotated
22

33
from fastapi import APIRouter, Path
4+
from redis import Redis
45

56
from src.api.dependencies import SettingsDep
67
from src.common import PasswordStorage
@@ -20,6 +21,11 @@
2021
]
2122

2223

23-
@v1_router.get("/haveibeenrocked/{prefix}")
24+
@v1_router.get("/haveibeenrocked/{prefix}") # TODO add response schema to openAPI
2425
def check_password_leak(prefix: APIPrefix, settings: SettingsDep):
25-
return {"Hello": "World"}
26+
redis_client = Redis.from_url(str(settings.kvrocks_url))
27+
password_storage = PasswordStorage(client=redis_client)
28+
29+
matching_password = password_storage.get_passwords(prefix=prefix)
30+
31+
return {prefix: matching_password}

src/common/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from pydantic import RedisDsn
2-
from pydantic.v1 import BaseSettings
2+
from pydantic_settings import BaseSettings
33

44

55
class Settings(BaseSettings):

tests/conftest.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
1+
import os
2+
from unittest import mock
3+
14
import pytest
25
from redis import Redis
36
from testcontainers.core.container import DockerContainer
47
from testcontainers.core.waiting_utils import wait_for_logs
58

9+
from src.api.dependencies import get_settings, get_settings_cached
10+
from src.api.main import app
11+
from src.common import PasswordStorage
12+
613
KVROCKS_IMAGE = "apache/kvrocks:2.11.0"
714

815

16+
@pytest.fixture(scope="session", autouse=True)
17+
def settings(): # disable the cache on settings, so it'll be reloaded everytime
18+
app.dependency_overrides[get_settings_cached] = get_settings
19+
try:
20+
yield get_settings # to give tests access to settings easily if needed
21+
finally:
22+
app.dependency_overrides = {}
23+
24+
925
@pytest.fixture(scope="session")
1026
def _kvrocks() -> Redis:
1127
with DockerContainer(image=KVROCKS_IMAGE).with_exposed_ports(6666) as kvrocks_container:
1228
wait_for_logs(kvrocks_container, "Ready to accept connections")
1329
host = kvrocks_container.get_container_host_ip()
1430
port = kvrocks_container.get_exposed_port(6666)
1531

16-
yield Redis(host=host, port=port)
32+
with mock.patch.dict(os.environ, {"KVROCKS_URL": f"redis://{host}:{port}"}):
33+
yield Redis(host=host, port=port)
1734

1835

1936
@pytest.fixture()
@@ -24,3 +41,10 @@ def kvrocks(_kvrocks) -> Redis:
2441
# Clean up keys after the test is done
2542
for key in _kvrocks.keys():
2643
_kvrocks.delete(key)
44+
45+
46+
@pytest.fixture
47+
def password_storage(kvrocks):
48+
password_storage_ = PasswordStorage(client=kvrocks)
49+
password_storage_.PIPELINE_MAX_SIZE = 1 # so we don't have to flush in our test
50+
yield password_storage_
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from fastapi.testclient import TestClient
2+
3+
from src.api.main import app
4+
5+
client = TestClient(app)
6+
7+
PREFIX_CHECKED = "123AB"
8+
9+
10+
def test_api_return_no_results(kvrocks):
11+
response = client.get(f"/api/v1/haveibeenrocked/{PREFIX_CHECKED}")
12+
13+
assert response.status_code == 200
14+
assert response.json() == {PREFIX_CHECKED: []}
15+
16+
17+
def test_api_return_one_result(kvrocks, password_storage):
18+
expected_password = "some password"
19+
password_storage.add_password(prefix=PREFIX_CHECKED, password=expected_password)
20+
21+
response = client.get(f"/api/v1/haveibeenrocked/{PREFIX_CHECKED}")
22+
23+
assert response.status_code == 200
24+
assert response.json() == {PREFIX_CHECKED: [expected_password]}
25+
26+
27+
def test_api_return_multiple_results(kvrocks, password_storage):
28+
expected_passwords = {
29+
"some password 1",
30+
"some password 2",
31+
"some password 3",
32+
}
33+
for password in expected_passwords:
34+
password_storage.add_password(prefix=PREFIX_CHECKED, password=password)
35+
36+
response = client.get(f"/api/v1/haveibeenrocked/{PREFIX_CHECKED}")
37+
38+
assert response.status_code == 200
39+
json_response = response.json()
40+
assert set(json_response.pop(PREFIX_CHECKED)) == expected_passwords
41+
assert len(json_response) == 0 # no other keys

tests/integration/api/test_api.py renamed to tests/integration/api/test_password_leak_validation.py

File renamed without changes.

tests/unit_test/common/test_password_storage.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,6 @@
55
SOME_PREFIX = "01234A"
66

77

8-
@pytest.fixture
9-
def password_storage(kvrocks):
10-
password_storage_ = PasswordStorage(client=kvrocks)
11-
password_storage_.PIPELINE_MAX_SIZE = 1 # so we don't have to flush in our test
12-
yield password_storage_
13-
14-
158
def test_get_password_should_return_empty_set_at_first(password_storage):
169
assert password_storage.get_passwords(SOME_PREFIX) == set()
1710

0 commit comments

Comments
 (0)