Refactor test system to be more extendable, add comment test

This commit is contained in:
Ben Rog-Wilhelm 2022-12-17 17:41:35 -08:00 committed by GitHub
parent 23a8fb9663
commit e257db1542
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 147 additions and 59 deletions

View file

@ -46,3 +46,6 @@ FROM release AS operation
# don't run the server itself, just start up the environment and assume we'll exec things from the outside # don't run the server itself, just start up the environment and assume we'll exec things from the outside
CMD sleep infinity CMD sleep infinity
# Turn off the rate limiter
ENV DBG_LIMITER_DISABLED=true

View file

@ -137,6 +137,9 @@ def get_remote_addr():
with app.app_context(): with app.app_context():
return request.headers.get('X-Real-IP', default='127.0.0.1') return request.headers.get('X-Real-IP', default='127.0.0.1')
app.config['RATE_LIMITER_ENABLED'] = not bool_from_string(environ.get('DBG_LIMITER_DISABLED', False))
if not app.config['RATE_LIMITER_ENABLED']:
print("Rate limiter disabled in debug mode!")
limiter = Limiter( limiter = Limiter(
app, app,
key_func=get_remote_addr, key_func=get_remote_addr,
@ -144,6 +147,7 @@ limiter = Limiter(
application_limits=["10/second;200/minute;5000/hour;10000/day"], application_limits=["10/second;200/minute;5000/hour;10000/day"],
storage_uri=environ.get("REDIS_URL", "redis://localhost"), storage_uri=environ.get("REDIS_URL", "redis://localhost"),
auto_check=False, auto_check=False,
enabled=app.config['RATE_LIMITER_ENABLED'],
) )
Base = declarative_base() Base = declarative_base()

View file

@ -261,8 +261,9 @@ def sign_up_post(v):
return redirect(f"/signup?{urlencode(args)}") return redirect(f"/signup?{urlencode(args)}")
if now - int(form_timestamp) < 5: if app.config['RATE_LIMITER_ENABLED']:
return signup_error("There was a problem. Please try again.") if now - int(form_timestamp) < 5:
return signup_error("There was a problem. Please try again.")
if not hmac.compare_digest(correct_formkey, form_formkey): if not hmac.compare_digest(correct_formkey, form_formkey):
return signup_error("There was a problem. Please try again.") return signup_error("There was a problem. Please try again.")

4
files/tests/conftest.py Normal file
View file

@ -0,0 +1,4 @@
import pytest
from .fixture_accounts import accounts

View file

@ -0,0 +1,43 @@
from . import util
from bs4 import BeautifulSoup
from files.__main__ import app
from functools import lru_cache
import pytest
from time import time, sleep
class AccountsFixture:
@lru_cache(maxsize=None)
def client_for_account(self, name = "default"):
client = app.test_client()
signup_get_response = client.get("/signup")
assert signup_get_response.status_code == 200
soup = BeautifulSoup(signup_get_response.text, 'html.parser')
# these hidden input values seem to be used for anti-bot purposes and need to be submitted
form_timestamp = next(tag for tag in soup.find_all("input") if tag.get("name") == "now").get("value")
username = f"test-{name}-{str(round(time()))}"
print(f"Signing up as {username}")
signup_post_response = client.post("/signup", data={
"username": username,
"password": "password",
"password_confirm": "password",
"email": "",
"formkey": util.formkey_from(signup_get_response.text),
"now": form_timestamp
})
assert signup_post_response.status_code == 302
assert "error" not in signup_post_response.location
return client
def logged_off(self):
return app.test_client()
@pytest.fixture(scope="session")
def accounts():
return AccountsFixture()

54
files/tests/test_basic.py Normal file
View file

@ -0,0 +1,54 @@
from . import fixture_accounts
from . import util
def test_rules(accounts):
response = accounts.logged_off().get("/rules")
assert response.status_code == 200
assert response.text.startswith("<!DOCTYPE html>")
def test_post_and_comment(accounts):
client = accounts.client_for_account()
# get our formkey
submit_get_response = client.get("/submit")
assert submit_get_response.status_code == 200
# make the post
post_title = util.generate_text()
post_body = util.generate_text()
submit_post_response = client.post("/submit", data={
"title": post_title,
"body": post_body,
"formkey": util.formkey_from(submit_get_response.text),
})
assert submit_post_response.status_code == 200
assert post_title in submit_post_response.text
assert post_body in submit_post_response.text
# verify it actually got posted
root_response = client.get("/")
assert root_response.status_code == 200
assert post_title in root_response.text
assert post_body in root_response.text
# yank the ID out
post = util.ItemData.from_html(submit_post_response.text)
# post a comment child
comment_body = util.generate_text()
submit_comment_response = client.post("/comment", data={
"parent_fullname": post.id_full,
"parent_level": 1,
"submission": post.id,
"body": comment_body,
"formkey": util.formkey_from(submit_post_response.text),
})
assert submit_comment_response.status_code == 200
# verify it actually got posted
post_response = client.get(post.url)
assert post_response.status_code == 200
assert comment_body in post_response.text

View file

@ -1,57 +0,0 @@
from bs4 import BeautifulSoup
from time import time, sleep
from files.__main__ import app
# these tests require `docker-compose up` first
def test_rules():
response = app.test_client().get("/rules")
assert response.status_code == 200
assert response.text.startswith("<!DOCTYPE html>")
def test_signup_and_post():
print("\nTesting signup and posting flow")
client = app.test_client()
with client: # this keeps the session between requests, which we need
signup_get_response = client.get("/signup")
assert signup_get_response.status_code == 200
soup = BeautifulSoup(signup_get_response.text, 'html.parser')
# these hidden input values seem to be used for anti-bot purposes and need to be submitted
formkey = next(tag for tag in soup.find_all("input") if tag.get("name") == "formkey").get("value")
form_timestamp = next(tag for tag in soup.find_all("input") if tag.get("name") == "now").get("value")
sleep(5) # too-fast submissions are rejected (bot check?)
username = "testuser" + str(round(time()))
signup_post_response = client.post("/signup", data={
"username": username,
"password": "password",
"password_confirm": "password",
"email": "",
"formkey": formkey,
"now": form_timestamp
})
print(f"Signing up as {username}")
assert signup_post_response.status_code == 302
assert "error" not in signup_post_response.location
# we should now be logged in and able to post
submit_get_response = client.get("/submit")
assert submit_get_response.status_code == 200
submit_post_response = client.post("/submit", data={
"title": "my_cool_post",
"body": "hey_guys",
})
assert submit_post_response.status_code == 302
post_render_result = client.get(submit_post_response.location)
assert "my_cool_post" in post_render_result.text
assert "hey_guys" in post_render_result.text

36
files/tests/util.py Normal file
View file

@ -0,0 +1,36 @@
from bs4 import BeautifulSoup
import random
import re
import string
def formkey_from(text):
soup = BeautifulSoup(text, 'html.parser')
formkey = next(tag for tag in soup.find_all("input") if tag.get("name") == "formkey").get("value")
return formkey
# not cryptographically secure, deal with it
def generate_text():
return ''.join(random.choices(string.ascii_lowercase, k=40))
# this is meant to be a utility class that stores post and comment references so you can use them in other calls
# it's pretty barebones and will probably be fleshed out
class ItemData:
id = None
id_full = None
url = None
@staticmethod
def from_html(text):
soup = BeautifulSoup(text, 'html.parser')
url = soup.find("meta", property="og:url")["content"]
match = re.search(r'/post/(\d+)/', url)
if match is None:
return None
result = ItemData()
result.id = match.group(1) # this really should get yanked out of the JS, not the URL
result.id_full = f"t2_{result.id}"
result.url = url
return result