forked from retoor/devplacepy
Add personal notes, DeepSearch history, backup offload, and gateway auth throttling
Also streamline the top navigation: drop the Tools dropdown, make Quizzes and Battles icon-only entries, and remove the Workspace, Containers and Editor entry points from the project detail page.
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import generate_uid
|
||||
|
||||
_counter_notes = [0]
|
||||
AJAX_notes = {"X-Requested-With": "fetch"}
|
||||
|
||||
|
||||
def _session_notes():
|
||||
_counter_notes[0] += 1
|
||||
name = f"nte{int(time.time() * 1000)}{_counter_notes[0]}"
|
||||
s = requests.Session()
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/signup",
|
||||
data={
|
||||
"username": name,
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s, name
|
||||
|
||||
|
||||
def _uid_notes(username):
|
||||
return get_table("users").find_one(username=username)["uid"]
|
||||
|
||||
|
||||
def _make_post_notes(owner_uid, title):
|
||||
uid = generate_uid()
|
||||
get_table("posts").insert(
|
||||
{
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
"uid": uid,
|
||||
"user_uid": owner_uid,
|
||||
"slug": f"{uid[-8:]}-note-post",
|
||||
"title": title,
|
||||
"content": "note target content",
|
||||
"topic": "random",
|
||||
"project_uid": None,
|
||||
"image": None,
|
||||
"stars": 0,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
)
|
||||
return uid
|
||||
|
||||
|
||||
def test_note_set_creates_row(app_server):
|
||||
s, name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(name), "Note target")
|
||||
r = s.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "Remember to check this later."},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["content"] == "Remember to check this later."
|
||||
row = get_table("notes").find_one(
|
||||
user_uid=_uid_notes(name), target_uid=post_uid, deleted_at=None
|
||||
)
|
||||
assert row is not None
|
||||
assert row["content"] == "Remember to check this later."
|
||||
|
||||
|
||||
def test_note_set_replaces_existing_content(app_server):
|
||||
s, name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(name), "Note replace target")
|
||||
s.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "First draft."},
|
||||
)
|
||||
s.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "Second draft."},
|
||||
)
|
||||
rows = list(
|
||||
get_table("notes").find(
|
||||
user_uid=_uid_notes(name), target_uid=post_uid, deleted_at=None
|
||||
)
|
||||
)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["content"] == "Second draft."
|
||||
|
||||
|
||||
def test_note_delete_soft_deletes(app_server):
|
||||
s, name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(name), "Note delete target")
|
||||
s.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "Delete me."},
|
||||
)
|
||||
r = s.post(f"{BASE_URL}/notes/post/{post_uid}/delete", headers=AJAX_notes)
|
||||
assert r.json() == {"deleted": True}
|
||||
assert (
|
||||
get_table("notes").count(
|
||||
user_uid=_uid_notes(name), target_uid=post_uid, deleted_at=None
|
||||
)
|
||||
== 0
|
||||
)
|
||||
row = get_table("notes").find_one(user_uid=_uid_notes(name), target_uid=post_uid)
|
||||
assert row is not None
|
||||
assert row["deleted_at"] is not None
|
||||
assert row["deleted_by"] == _uid_notes(name)
|
||||
|
||||
|
||||
def test_note_revives_after_delete(app_server):
|
||||
s, name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(name), "Note revive target")
|
||||
s.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "Before delete."},
|
||||
)
|
||||
s.post(f"{BASE_URL}/notes/post/{post_uid}/delete", headers=AJAX_notes)
|
||||
s.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "After revive."},
|
||||
)
|
||||
rows = list(get_table("notes").find(user_uid=_uid_notes(name), target_uid=post_uid))
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["deleted_at"] is None
|
||||
assert rows[0]["content"] == "After revive."
|
||||
|
||||
|
||||
def test_note_requires_login(app_server):
|
||||
owner_s, owner_name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(owner_name), "Needs login")
|
||||
anon = requests.Session()
|
||||
r = anon.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "sneaky"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303
|
||||
assert get_table("notes").count(target_uid=post_uid) == 0
|
||||
|
||||
|
||||
def test_note_invalid_target_type_returns_400(app_server):
|
||||
s, name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(name), "Bad target")
|
||||
r = s.post(
|
||||
f"{BASE_URL}/notes/widget/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "does not matter"},
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_note_is_private_per_user(app_server):
|
||||
alice, alice_name = _session_notes()
|
||||
bob, bob_name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(alice_name), "Shared target")
|
||||
alice.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "Alice's private note."},
|
||||
)
|
||||
bob.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "Bob's private note."},
|
||||
)
|
||||
alice_row = get_table("notes").find_one(
|
||||
user_uid=_uid_notes(alice_name), target_uid=post_uid, deleted_at=None
|
||||
)
|
||||
bob_row = get_table("notes").find_one(
|
||||
user_uid=_uid_notes(bob_name), target_uid=post_uid, deleted_at=None
|
||||
)
|
||||
assert alice_row["content"] == "Alice's private note."
|
||||
assert bob_row["content"] == "Bob's private note."
|
||||
|
||||
bob_view = bob.get(
|
||||
f"{BASE_URL}/posts/{post_uid}", headers={"Accept": "application/json"}
|
||||
)
|
||||
assert bob_view.json()["note_content"] == "Bob's private note."
|
||||
|
||||
|
||||
def test_note_delete_only_affects_own_row(app_server):
|
||||
alice, alice_name = _session_notes()
|
||||
bob, bob_name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(alice_name), "Delete isolation target")
|
||||
alice.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "Alice keeps this."},
|
||||
)
|
||||
bob.post(f"{BASE_URL}/notes/post/{post_uid}/delete", headers=AJAX_notes)
|
||||
alice_row = get_table("notes").find_one(
|
||||
user_uid=_uid_notes(alice_name), target_uid=post_uid, deleted_at=None
|
||||
)
|
||||
assert alice_row is not None
|
||||
assert alice_row["content"] == "Alice keeps this."
|
||||
@@ -0,0 +1,89 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import generate_uid
|
||||
|
||||
_counter_notes = [0]
|
||||
AJAX_notes = {"X-Requested-With": "fetch"}
|
||||
|
||||
|
||||
def _session_notes():
|
||||
_counter_notes[0] += 1
|
||||
name = f"ntesv{int(time.time() * 1000)}{_counter_notes[0]}"
|
||||
s = requests.Session()
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/signup",
|
||||
data={
|
||||
"username": name,
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
"birth_date": "1990-01-01",
|
||||
"accept_terms": "1",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s, name
|
||||
|
||||
|
||||
def _uid_notes(username):
|
||||
return get_table("users").find_one(username=username)["uid"]
|
||||
|
||||
|
||||
def _make_post_notes(owner_uid, title):
|
||||
uid = generate_uid()
|
||||
get_table("posts").insert(
|
||||
{
|
||||
"deleted_at": None,
|
||||
"deleted_by": None,
|
||||
"uid": uid,
|
||||
"user_uid": owner_uid,
|
||||
"slug": f"{uid[:8]}-note-saved-post",
|
||||
"title": title,
|
||||
"content": "note saved target content",
|
||||
"topic": "random",
|
||||
"project_uid": None,
|
||||
"image": None,
|
||||
"stars": 0,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
)
|
||||
return uid
|
||||
|
||||
|
||||
def test_notes_saved_page_lists_note_content(app_server):
|
||||
s, name = _session_notes()
|
||||
owner_uid = _uid_notes(name)
|
||||
post_title = f"Note saved post {int(time.time() * 1000)}"
|
||||
post_uid = _make_post_notes(owner_uid, post_title)
|
||||
s.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "A note worth remembering."},
|
||||
)
|
||||
html = s.get(f"{BASE_URL}/notes/saved").text
|
||||
assert post_title in html
|
||||
assert "A note worth remembering." in html
|
||||
|
||||
|
||||
def test_notes_saved_page_requires_login(app_server):
|
||||
anon = requests.Session()
|
||||
r = anon.get(f"{BASE_URL}/notes/saved", allow_redirects=False)
|
||||
assert r.status_code == 303
|
||||
|
||||
|
||||
def test_notes_saved_page_only_shows_own_notes(app_server):
|
||||
alice, alice_name = _session_notes()
|
||||
bob, bob_name = _session_notes()
|
||||
post_uid = _make_post_notes(_uid_notes(alice_name), "Isolation saved post")
|
||||
alice.post(
|
||||
f"{BASE_URL}/notes/post/{post_uid}",
|
||||
headers=AJAX_notes,
|
||||
data={"content": "Alice only note."},
|
||||
)
|
||||
html = bob.get(f"{BASE_URL}/notes/saved").text
|
||||
assert "Alice only note." not in html
|
||||
Reference in New Issue
Block a user