7 Commits

Author SHA1 Message Date
Jack Case
cbabd5da3c testing top offenders query 2025-11-26 21:43:18 +00:00
Jack Case
283e9c6b7f Merge branch 'feat_testing' into feat_domain_preload 2025-11-26 21:15:53 +00:00
Jack Case
1cb9ae8606 writing some unit tests 2025-11-26 20:44:02 +00:00
Jack Case
6a1837762e move testing-related files to test submodule
create scripts to load and dump the test database to and from SQL files
2025-11-26 16:19:14 +00:00
Jack Case
fcc5d9d7bc WIP: wrote a query to get domains ordered by number of reported paths 2025-11-24 18:59:22 +00:00
Jack Case
a36b6e9865 actually send the email 2025-11-15 16:04:45 +00:00
Jack Case
20fffc85c1 check if email is verified when authorizing 2025-11-15 15:53:02 +00:00
10 changed files with 98 additions and 5 deletions

3
.gitignore vendored
View File

@@ -214,4 +214,7 @@ __marimo__/
# Streamlit # Streamlit
.streamlit/secrets.toml .streamlit/secrets.toml
slopserver/server_config.env slopserver/server_config.env
test_db.sqlite

View File

@@ -1,7 +1,7 @@
from collections.abc import Iterable from collections.abc import Iterable
from datetime import datetime from datetime import datetime
from urllib.parse import ParseResult from urllib.parse import ParseResult
from sqlalchemy import select from sqlalchemy import select, func
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from slopserver.models import Domain, Path, User, Report from slopserver.models import Domain, Path, User, Report
@@ -11,6 +11,13 @@ def select_slop(urls: list[ParseResult], engine: Engine) -> Iterable[Domain]:
with Session(engine) as session: with Session(engine) as session:
rows = session.scalars(query).all() rows = session.scalars(query).all()
return rows return rows
def top_offenders(engine: Engine, limit: int|None = None) -> Iterable[Domain]:
query = select(Domain.domain_name, func.count(Path.id)).join(Path).group_by(Domain.id).order_by(func.count(Path.id).desc())
if limit: query = query.limit(limit)
with Session(engine) as session:
top_offenders = session.execute(query).all()
return top_offenders
def insert_slop(urls: list[ParseResult], engine: Engine, user: User | None = None): def insert_slop(urls: list[ParseResult], engine: Engine, user: User | None = None):
domain_dict: dict[str. set[str]] = dict() domain_dict: dict[str. set[str]] = dict()

View File

@@ -34,6 +34,7 @@ from slopserver.settings import settings
from slopserver.models import Domain, Path, User from slopserver.models import Domain, Path, User
from slopserver.models import SlopReport, SignupForm, altcha_validator from slopserver.models import SlopReport, SignupForm, altcha_validator
from slopserver.db import select_slop, insert_slop, get_user, create_user, verify_user_email from slopserver.db import select_slop, insert_slop, get_user, create_user, verify_user_email
from slopserver.email import generate_verification_email, send_email
app = FastAPI() app = FastAPI()
@@ -150,9 +151,10 @@ def signup_form(form_data: Annotated[SignupForm, Form()]):
# send verification email # send verification email
# create a jwt encoding the username and a time limit to be the verification URL # create a jwt encoding the username and a time limit to be the verification URL
token = generate_verification_token(form_data.email) token = generate_verification_token(form_data.email)
return token
email_html = generate_verification_email(settings.api_base + "verify/?token=" + token)
status = send_email(form_data.email, "Slop Farmer Email Verification", email_html)
return status
@app.get("/verify") @app.get("/verify")
def verify_email(token: Annotated[str, AfterValidator(verify_verification_token)]): def verify_email(token: Annotated[str, AfterValidator(verify_verification_token)]):
@@ -191,6 +193,8 @@ def simple_login(username: Annotated[str, Form()], password: Annotated[str, Form
user = auth_user(username, password, DB_ENGINE) user = auth_user(username, password, DB_ENGINE)
if not user: if not user:
raise HTTPException(status_code=401, detail="Incorrect username or password") raise HTTPException(status_code=401, detail="Incorrect username or password")
if not user.email_verified:
raise HTTPException(status_code=401, detail="Unverified email address")
token = generate_auth_token(username) token = generate_auth_token(username)
return {"access_token": token, "token_type": "bearer"} return {"access_token": token, "token_type": "bearer"}

View File

@@ -2,11 +2,12 @@ from pydantic_settings import BaseSettings
class ServerSettings(BaseSettings): class ServerSettings(BaseSettings):
db_url: str = "sqlite+pysqlite:///test_db.sqlite" db_url: str = "sqlite+pysqlite:///slopserver/test/test_db.sqlite"
token_secret: str = "5bcc778a96b090c3ac1d587bb694a060eaf7bdb5832365f91d5078faf1fff210" token_secret: str = "5bcc778a96b090c3ac1d587bb694a060eaf7bdb5832365f91d5078faf1fff210"
altcha_secret: str = "0460de065912d0292df1e7422a5ed2dc362ed56d6bab64fe50b89957463061f3" altcha_secret: str = "0460de065912d0292df1e7422a5ed2dc362ed56d6bab64fe50b89957463061f3"
resend_token: str = "re_NXpjzbqR_KgAbu72PKjYHcquX24WvnN3i" resend_token: str = "re_NXpjzbqR_KgAbu72PKjYHcquX24WvnN3i"
sender_email: str = "slopfarmer@jack-case.pro" sender_email: str = "slopfarmer@jack-case.pro"
api_base: str = "api.slopfarmer.jack-case.pro/"
settings = ServerSettings() settings = ServerSettings()

View File

View File

@@ -0,0 +1,5 @@
#!/bin/bash
test_dir=slopserver/test
sqlite3 $test_dir/test_db.sqlite .dump > $1

View File

@@ -0,0 +1,5 @@
#!/bin/bash
test_dir=slopserver/test
cat $test_dir/test_db.sql | sqlite3 $test_dir/test_db.sqlite

View File

@@ -0,0 +1,5 @@
from fastapi import FastAPI
from fastapi.testclient import TestClient
from slopserver.server import app as slopserver_app
client = TestClient(slopserver_app)

View File

@@ -0,0 +1,18 @@
from slopserver.db import *
from slopserver.models import *
from slopserver.settings import settings
from sqlalchemy import create_engine
import unittest
class TestDBFuncs(unittest.TestCase):
test_db_url = settings.db_url
def setUp(self):
self.engine = create_engine(self.test_db_url)
def test_get_top_offenders(self):
items = top_offenders(self.engine)
print(items)
self.assertEqual(items[0][0], "moogle.com")

View File

@@ -0,0 +1,45 @@
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE domain (
id INTEGER NOT NULL,
domain_name VARCHAR NOT NULL,
CONSTRAINT pk_domain PRIMARY KEY (id)
);
INSERT INTO domain VALUES(1,'google.com');
INSERT INTO domain VALUES(2,'moogle.com');
CREATE TABLE user (
id INTEGER NOT NULL,
email VARCHAR NOT NULL,
password_hash VARCHAR NOT NULL,
email_verified BOOLEAN NOT NULL,
CONSTRAINT pk_user PRIMARY KEY (id)
);
INSERT INTO user VALUES(1,'alphauser01','$argon2id$v=19$m=65536,t=3,p=4$3z7uxa3NHl/dKG07RGEvBA$0NOBftJpP+HiR7wfgdwBk2UR9F12YBjrqeqLSyDl47o','True');
CREATE TABLE path (
id INTEGER NOT NULL,
path VARCHAR NOT NULL,
domain_id INTEGER,
CONSTRAINT pk_path PRIMARY KEY (id),
CONSTRAINT fk_path_domain_id_domain FOREIGN KEY(domain_id) REFERENCES domain (id)
);
INSERT INTO path VALUES(1,'/',0);
INSERT INTO path VALUES(2,'/path1',0);
INSERT INTO path VALUES(3,'/path2',1);
INSERT INTO path VALUES(4,'/path3/a',1);
INSERT INTO path VALUES(5,'/path4',1);
INSERT INTO path VALUES(6,'/path2',2);
INSERT INTO path VALUES(7,'/',2);
INSERT INTO path VALUES(8,'/path3',2);
INSERT INTO path VALUES(9,'/path4',2);
CREATE TABLE report (
path_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
timestamp DATETIME,
CONSTRAINT pk_report PRIMARY KEY (path_id, user_id),
CONSTRAINT fk_report_path_id_path FOREIGN KEY(path_id) REFERENCES path (id),
CONSTRAINT fk_report_user_id_user FOREIGN KEY(user_id) REFERENCES user (id)
);
INSERT INTO report VALUES(2,1,'11-26-2025');
CREATE UNIQUE INDEX ix_domain_domain_name ON domain (domain_name);
CREATE UNIQUE INDEX ix_user_email ON user (email);
COMMIT;