smodusermc commited on
Commit
e163512
·
verified ·
1 Parent(s): a40ecea

Create test_social.py

Browse files
Files changed (1) hide show
  1. tests/test_social.py +169 -0
tests/test_social.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Focused end-to-end checks for the social media feature set.
3
+
4
+ Runs against a live server on 127.0.0.1:8899. Start it with a fresh writable
5
+ DATABASE_URL before running (see README/test harness examples).
6
+ """
7
+ import asyncio
8
+ import json
9
+ import os
10
+ import sys
11
+ import uuid
12
+
13
+ import httpx
14
+
15
+ BASE = "http://127.0.0.1:8899"
16
+ failures = []
17
+
18
+
19
+ def check(cond, label, extra=""):
20
+ if cond:
21
+ print(f" \u2713 {label}")
22
+ else:
23
+ failures.append(label + (f" \u2014 {extra}" if extra else ""))
24
+ print(f" \u2717 {label} {extra}")
25
+
26
+
27
+ async def signup(client, username):
28
+ r = await client.post(f"{BASE}/api/auth/signup", params={
29
+ "username": username,
30
+ "password": "password123",
31
+ "display_name": username.title(),
32
+ })
33
+ return r
34
+
35
+
36
+ async def upload_file(client, token, name, data, mime):
37
+ params = {
38
+ "chunk_index": 0,
39
+ "total_chunks": 1,
40
+ "file_name": name,
41
+ "file_type": mime,
42
+ "file_size": len(data),
43
+ "upload_id": uuid.uuid4().hex,
44
+ }
45
+ r = await client.post(
46
+ f"{BASE}/api/upload/chunk",
47
+ params=params,
48
+ files={"file": (name, data, mime)},
49
+ headers={"X-Auth-Token": token},
50
+ )
51
+ if r.status_code != 200:
52
+ return None, r
53
+ return r.json()["file_path"], r
54
+
55
+
56
+ async def main():
57
+ async with httpx.AsyncClient(timeout=20) as http:
58
+ # Unique usernames so the test can be run repeatedly.
59
+ u1 = "soc_" + uuid.uuid4().hex[:8]
60
+ u2 = "soc_" + uuid.uuid4().hex[:8]
61
+
62
+ r = await signup(http, u1)
63
+ check(r.status_code == 200 and r.json().get("token"), f"signup {u1}", r.text[:160])
64
+ t1 = r.json()["token"]
65
+ h1 = {"X-Auth-Token": t1}
66
+
67
+ r = await signup(http, u2)
68
+ check(r.status_code == 200 and r.json().get("token"), f"signup {u2}", r.text[:160])
69
+ t2 = r.json()["token"]
70
+ h2 = {"X-Auth-Token": t2}
71
+
72
+ # --- image upload through the shared chunked endpoint + post create ---
73
+ # Minimal 1x1 PNG.
74
+ png = (
75
+ b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
76
+ b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc\xf8\xcf\xc0"
77
+ b"\x00\x00\x00\x03\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
78
+ b"\x00\x00\x00\x00IEND\xaeB`\x82"
79
+ )
80
+ fp, up = await upload_file(http, t1, "pic.png", png, "image/png")
81
+ check(up.status_code == 200 and fp, "chunked image upload returned a path", up.text[:160])
82
+
83
+ media = [{"file_path": fp, "file_name": "pic.png", "file_type": "image/png", "file_size": len(png)}]
84
+ r = await http.post(
85
+ f"{BASE}/api/social/posts",
86
+ params={"body": "Hello social world", "media_json": json.dumps(media)},
87
+ headers=h1,
88
+ )
89
+ check(r.status_code == 200 and len(r.json().get("post", {}).get("media", [])) == 1,
90
+ "post with image created", r.text[:160])
91
+ post_id = r.json()["post"]["id"]
92
+
93
+ # --- text attachments are allowed by the shared uploader but rejected on posts ---
94
+ txt = b"just some notes"
95
+ tfp, up = await upload_file(http, t1, "notes.txt", txt, "text/plain")
96
+ check(up.status_code == 200 and tfp, "chunked text upload still works for chat", up.text[:160])
97
+ bad_media = [{"file_path": tfp, "file_name": "notes.txt", "file_type": "text/plain", "file_size": len(txt)}]
98
+ r = await http.post(
99
+ f"{BASE}/api/social/posts",
100
+ params={"body": "should fail", "media_json": json.dumps(bad_media)},
101
+ headers=h1,
102
+ )
103
+ check(r.status_code == 400 and "images and videos" in (r.json().get("detail") or ""),
104
+ "text media rejected on social posts", r.text[:160])
105
+
106
+ # --- edit own post ---
107
+ r = await http.patch(f"{BASE}/api/social/posts/{post_id}", params={"body": "Edited body"}, headers=h1)
108
+ check(r.status_code == 200 and r.json()["post"]["body"] == "Edited body"
109
+ and r.json()["post"].get("edited_at_ms"),
110
+ "author can edit own post", r.text[:160])
111
+
112
+ # --- non-author cannot edit ---
113
+ r = await http.patch(f"{BASE}/api/social/posts/{post_id}", params={"body": "nope"}, headers=h2)
114
+ check(r.status_code == 403, "non-author cannot edit", r.text[:160])
115
+
116
+ # --- like / repost / bookmark toggles ---
117
+ r = await http.post(f"{BASE}/api/social/posts/{post_id}/like", headers=h2)
118
+ check(r.status_code == 200 and r.json()["liked"] and r.json()["like_count"] == 1,
119
+ "second user can like", r.text[:160])
120
+ r = await http.post(f"{BASE}/api/social/posts/{post_id}/repost", headers=h2)
121
+ check(r.status_code == 200 and r.json()["reposted"], "second user can repost", r.text[:160])
122
+ r = await http.get(f"{BASE}/api/social/feed", params={"feed": "home", "limit": 50}, headers=h2)
123
+ repost = next((p for p in (r.json().get("posts") or [])
124
+ if p["id"] == post_id and p.get("reposter_id")), None)
125
+ check(repost is not None and repost.get("reposter", {}).get("username") == u2,
126
+ "repost carries reposter info", r.text[:160])
127
+ r = await http.post(f"{BASE}/api/social/posts/{post_id}/bookmark", headers=h2)
128
+ check(r.status_code == 200 and r.json()["bookmarked"], "second user can bookmark", r.text[:160])
129
+
130
+ # --- search shows follow state truthfully ---
131
+ await http.post(f"{BASE}/api/social/users/{u1}/follow", headers=h2)
132
+ r = await http.get(f"{BASE}/api/social/search", params={"q": u1}, headers=h2)
133
+ hit = next((u for u in (r.json().get("users") or []) if u["username"] == u1), None)
134
+ check(r.status_code == 200 and hit and hit["is_following"],
135
+ "search reflects follow status", r.text[:160])
136
+
137
+ # --- delete own post ---
138
+ r = await http.delete(f"{BASE}/api/social/posts/{post_id}", headers=h1)
139
+ check(r.status_code == 200, "author can delete post", r.text[:160])
140
+ r = await http.get(f"{BASE}/api/social/posts/{post_id}", headers=h1)
141
+ check(r.status_code == 404, "deleted post returns 404", r.text[:160])
142
+
143
+ # --- backup admin endpoints are admin-only ---
144
+ admin_user = os.environ.get("ADMIN_USERNAME", "").strip()
145
+ admin_pass = os.environ.get("ADMIN_PASSWORD", "")
146
+ if admin_user and admin_pass:
147
+ r = await http.post(f"{BASE}/api/admin/login", json={"username": admin_user, "password": admin_pass})
148
+ check(r.status_code == 200 and r.json().get("token"), "admin login succeeds", r.text[:160])
149
+ admin_headers = {"X-Admin-Token": r.json()["token"]}
150
+ r = await http.get(f"{BASE}/api/admin/backups", headers=admin_headers)
151
+ check(r.status_code == 200, "admin backup list reachable", r.text[:160])
152
+ r = await http.get(f"{BASE}/api/admin/backups", headers=h1)
153
+ check(r.status_code == 401, "normal user cannot access admin backups", r.text[:160])
154
+ else:
155
+ r = await http.get(f"{BASE}/api/admin/backups", headers=h1)
156
+ check(r.status_code == 401, "backup admin requires dedicated admin auth", r.text[:160])
157
+
158
+ print(f"\nfinished. failures={len(failures)}")
159
+ print("ALL SOCIAL CHECKS PASSED" if not failures else "SOCIAL CHECKS FAILED")
160
+ return failures
161
+
162
+
163
+ if __name__ == "__main__":
164
+ fails = asyncio.run(main())
165
+ if fails:
166
+ for f in fails:
167
+ print(" -", f)
168
+ sys.exit(1)
169
+ sys.exit(0)