forked from retoor/devplacepy
feat: add plug-and-play devRant API client examples with Python and JavaScript
Add complete set of example scripts for the devRant REST protocol under examples/devrant/, including reusable client libraries, a rant poster, a live feed watcher with keyword-based auto-upvote, and an end-to-end smoke test that exercises every endpoint. Both Python (stdlib-only) and JavaScript (Node 18+ global fetch) implementations are provided.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# DevPlace devRant API examples
|
||||
|
||||
Plug-and-play clients and scripts for the devRant-compatible REST protocol served at
|
||||
`/api`. The Python scripts use only the standard library (`urllib`); the JavaScript
|
||||
scripts use Node 18+ (global `fetch`, ES modules). No dependencies to install. Full
|
||||
reference: `/docs/devrant.html`.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Language | What it does |
|
||||
|------|----------|--------------|
|
||||
| `client.py` / `client.mjs` | Python / JS | Reusable `DevRant` client: login, token injection, every endpoint. Imported by the others. |
|
||||
| `post_rant.py` / `post_rant.mjs` | Python / JS | Log in and post one rant from the command line. |
|
||||
| `feed_watch.py` / `feed_watch.mjs` | Python / JS | Live feed ticker; optionally auto-upvotes rants matching keywords. |
|
||||
| `smoke_test.py` / `smoke_test.mjs` | Python / JS | End-to-end conformance test: registers a throwaway user and exercises every endpoint, printing PASS/FAIL. |
|
||||
|
||||
## Configuration
|
||||
|
||||
The scripts read their settings from environment variables:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `DEVRANT_BASE` | `http://localhost:10500` | Base URL of the DevPlace server (no `/api` suffix) |
|
||||
| `DEVRANT_USERNAME` | (none) | Username for write operations |
|
||||
| `DEVRANT_PASSWORD` | (none) | Password for write operations |
|
||||
| `DEVRANT_POLL_SECONDS` | `30` | `feed_watch` poll interval |
|
||||
| `DEVRANT_UPVOTE_KEYWORDS` | (empty) | Comma-separated keywords `feed_watch` auto-upvotes |
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# post a rant
|
||||
DEVRANT_USERNAME=you DEVRANT_PASSWORD=secret6 \
|
||||
python post_rant.py "Posted from a script" "python,automation"
|
||||
|
||||
# same in JavaScript
|
||||
DEVRANT_USERNAME=you DEVRANT_PASSWORD=secret6 \
|
||||
node post_rant.mjs "Posted from a script" "python,automation"
|
||||
|
||||
# watch the feed and auto-upvote anything mentioning rust
|
||||
DEVRANT_USERNAME=you DEVRANT_PASSWORD=secret6 DEVRANT_UPVOTE_KEYWORDS=rust \
|
||||
python feed_watch.py
|
||||
|
||||
# run the full conformance test against a running server
|
||||
DEVRANT_BASE=http://localhost:10500 python smoke_test.py
|
||||
DEVRANT_BASE=http://localhost:10500 node smoke_test.mjs
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Authentication follows the devRant model: `login()` calls `POST /api/users/auth-token`
|
||||
and stores the `(user_id, token_id, token_key)` triple, which is then sent with every
|
||||
call (query params for `GET`/`DELETE`, form body for `POST`).
|
||||
- Read endpoints (feed, single rant, search, profiles) work without logging in.
|
||||
- These scripts target a DevPlace server. A legacy devRant client hard-coded to
|
||||
`devrant.com` is reached only through host routing (DNS/reverse-proxy), which is an
|
||||
infrastructure concern outside these examples.
|
||||
@@ -0,0 +1,126 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
export class DevRant {
|
||||
constructor(baseUrl, username, password) {
|
||||
this.baseUrl = baseUrl.replace(/\/$/, "");
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.auth = {};
|
||||
}
|
||||
|
||||
_url(path, params) {
|
||||
const url = new URL(`${this.baseUrl}/api/${path.replace(/^\//, "")}`);
|
||||
for (const [key, value] of Object.entries(params || {})) {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
async _request(method, path, params = {}, body = null) {
|
||||
const merged = { ...params, ...this.auth };
|
||||
const headers = { Accept: "application/json" };
|
||||
let url;
|
||||
const init = { method, headers };
|
||||
if (method === "GET" || method === "DELETE") {
|
||||
url = this._url(path, merged);
|
||||
} else {
|
||||
url = this._url(path);
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
init.body = new URLSearchParams({ ...merged, ...(body || {}) }).toString();
|
||||
}
|
||||
const response = await fetch(url, init);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async login() {
|
||||
const result = await this._request("POST", "users/auth-token", {}, {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
});
|
||||
if (!result.success) throw new Error(result.error || "login failed");
|
||||
const token = result.auth_token;
|
||||
this.auth = { user_id: token.user_id, token_id: token.id, token_key: token.key };
|
||||
return this.auth;
|
||||
}
|
||||
|
||||
register(email) {
|
||||
return this._request("POST", "users", {}, {
|
||||
username: this.username,
|
||||
email,
|
||||
password: this.password,
|
||||
});
|
||||
}
|
||||
|
||||
async rants(sort = "recent", limit = 20, skip = 0) {
|
||||
const result = await this._request("GET", "devrant/rants", { sort, limit, skip });
|
||||
return result.rants || [];
|
||||
}
|
||||
|
||||
rant(rantId) {
|
||||
return this._request("GET", `devrant/rants/${rantId}`);
|
||||
}
|
||||
|
||||
async search(term) {
|
||||
return (await this._request("GET", "devrant/search", { term })).results || [];
|
||||
}
|
||||
|
||||
async userId(username) {
|
||||
return (await this._request("GET", "get-user-id", { username })).user_id;
|
||||
}
|
||||
|
||||
async profile(userId) {
|
||||
return (await this._request("GET", `users/${userId}`)).profile;
|
||||
}
|
||||
|
||||
async notifs() {
|
||||
return (await this._request("GET", "users/me/notif-feed")).data || {};
|
||||
}
|
||||
|
||||
clearNotifs() {
|
||||
return this._request("DELETE", "users/me/notif-feed");
|
||||
}
|
||||
|
||||
editProfile(fields) {
|
||||
return this._request("POST", "users/me/edit-profile", {}, fields);
|
||||
}
|
||||
|
||||
postRant(text, tags = "") {
|
||||
return this._request("POST", "devrant/rants", {}, { rant: text, tags });
|
||||
}
|
||||
|
||||
editRant(rantId, text, tags = "") {
|
||||
return this._request("POST", `devrant/rants/${rantId}`, {}, { rant: text, tags });
|
||||
}
|
||||
|
||||
deleteRant(rantId) {
|
||||
return this._request("DELETE", `devrant/rants/${rantId}`);
|
||||
}
|
||||
|
||||
voteRant(rantId, vote) {
|
||||
return this._request("POST", `devrant/rants/${rantId}/vote`, {}, { vote });
|
||||
}
|
||||
|
||||
favorite(rantId) {
|
||||
return this._request("POST", `devrant/rants/${rantId}/favorite`);
|
||||
}
|
||||
|
||||
unfavorite(rantId) {
|
||||
return this._request("POST", `devrant/rants/${rantId}/unfavorite`);
|
||||
}
|
||||
|
||||
comment(rantId, text) {
|
||||
return this._request("POST", `devrant/rants/${rantId}/comments`, {}, { comment: text });
|
||||
}
|
||||
|
||||
voteComment(commentId, vote) {
|
||||
return this._request("POST", `comments/${commentId}/vote`, {}, { vote });
|
||||
}
|
||||
}
|
||||
|
||||
export function fromEnv() {
|
||||
return new DevRant(
|
||||
process.env.DEVRANT_BASE || "http://localhost:10500",
|
||||
process.env.DEVRANT_USERNAME,
|
||||
process.env.DEVRANT_PASSWORD,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class DevRant:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
) -> None:
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.auth: dict[str, Any] = {}
|
||||
|
||||
def _url(self, path: str) -> str:
|
||||
return f"{self.base_url}/api/{path.lstrip('/')}"
|
||||
|
||||
def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
params: Optional[dict] = None,
|
||||
body: Optional[dict] = None,
|
||||
) -> dict:
|
||||
merged = dict(params or {})
|
||||
merged.update(self.auth)
|
||||
url = self._url(path)
|
||||
data = None
|
||||
headers = {"Accept": "application/json"}
|
||||
if method in ("GET", "DELETE"):
|
||||
if merged:
|
||||
url += "?" + urllib.parse.urlencode(merged)
|
||||
else:
|
||||
payload = dict(merged)
|
||||
payload.update(body or {})
|
||||
data = urllib.parse.urlencode(payload).encode("utf-8")
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
||||
request = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
with urllib.request.urlopen(request) as response:
|
||||
return json.loads(response.read().decode("utf-8"))
|
||||
|
||||
def login(self) -> dict:
|
||||
result = self._request(
|
||||
"POST",
|
||||
"users/auth-token",
|
||||
body={"username": self.username, "password": self.password},
|
||||
)
|
||||
if not result.get("success"):
|
||||
raise RuntimeError(result.get("error", "login failed"))
|
||||
token = result["auth_token"]
|
||||
self.auth = {
|
||||
"user_id": token["user_id"],
|
||||
"token_id": token["id"],
|
||||
"token_key": token["key"],
|
||||
}
|
||||
return self.auth
|
||||
|
||||
def register(self, email: str) -> dict:
|
||||
return self._request(
|
||||
"POST",
|
||||
"users",
|
||||
body={
|
||||
"username": self.username,
|
||||
"email": email,
|
||||
"password": self.password,
|
||||
},
|
||||
)
|
||||
|
||||
def rants(self, sort: str = "recent", limit: int = 20, skip: int = 0) -> list:
|
||||
result = self._request(
|
||||
"GET", "devrant/rants", {"sort": sort, "limit": limit, "skip": skip}
|
||||
)
|
||||
return result.get("rants", [])
|
||||
|
||||
def rant(self, rant_id: int) -> dict:
|
||||
return self._request("GET", f"devrant/rants/{rant_id}")
|
||||
|
||||
def search(self, term: str) -> list:
|
||||
return self._request("GET", "devrant/search", {"term": term}).get("results", [])
|
||||
|
||||
def user_id(self, username: str) -> Optional[int]:
|
||||
return self._request("GET", "get-user-id", {"username": username}).get("user_id")
|
||||
|
||||
def profile(self, user_id: int) -> Optional[dict]:
|
||||
return self._request("GET", f"users/{user_id}").get("profile")
|
||||
|
||||
def notifs(self) -> dict:
|
||||
return self._request("GET", "users/me/notif-feed").get("data", {})
|
||||
|
||||
def clear_notifs(self) -> dict:
|
||||
return self._request("DELETE", "users/me/notif-feed")
|
||||
|
||||
def edit_profile(self, **fields: str) -> dict:
|
||||
return self._request("POST", "users/me/edit-profile", body=fields)
|
||||
|
||||
def post_rant(self, text: str, tags: str = "") -> dict:
|
||||
return self._request("POST", "devrant/rants", body={"rant": text, "tags": tags})
|
||||
|
||||
def edit_rant(self, rant_id: int, text: str, tags: str = "") -> dict:
|
||||
return self._request(
|
||||
"POST", f"devrant/rants/{rant_id}", body={"rant": text, "tags": tags}
|
||||
)
|
||||
|
||||
def delete_rant(self, rant_id: int) -> dict:
|
||||
return self._request("DELETE", f"devrant/rants/{rant_id}")
|
||||
|
||||
def vote_rant(self, rant_id: int, vote: int) -> dict:
|
||||
return self._request(
|
||||
"POST", f"devrant/rants/{rant_id}/vote", body={"vote": vote}
|
||||
)
|
||||
|
||||
def favorite(self, rant_id: int) -> dict:
|
||||
return self._request("POST", f"devrant/rants/{rant_id}/favorite")
|
||||
|
||||
def unfavorite(self, rant_id: int) -> dict:
|
||||
return self._request("POST", f"devrant/rants/{rant_id}/unfavorite")
|
||||
|
||||
def comment(self, rant_id: int, text: str) -> dict:
|
||||
return self._request(
|
||||
"POST", f"devrant/rants/{rant_id}/comments", body={"comment": text}
|
||||
)
|
||||
|
||||
def vote_comment(self, comment_id: int, vote: int) -> dict:
|
||||
return self._request("POST", f"comments/{comment_id}/vote", body={"vote": vote})
|
||||
|
||||
|
||||
def from_env() -> DevRant:
|
||||
return DevRant(
|
||||
os.environ.get("DEVRANT_BASE", "http://localhost:10500"),
|
||||
os.environ.get("DEVRANT_USERNAME"),
|
||||
os.environ.get("DEVRANT_PASSWORD"),
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { fromEnv } from "./client.mjs";
|
||||
|
||||
const pollSeconds = Number(process.env.DEVRANT_POLL_SECONDS || "30");
|
||||
const keywords = (process.env.DEVRANT_UPVOTE_KEYWORDS || "")
|
||||
.split(",")
|
||||
.map((word) => word.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const api = fromEnv();
|
||||
if (api.username && api.password) await api.login();
|
||||
|
||||
const seen = new Set();
|
||||
console.log(`watching feed every ${pollSeconds}s (ctrl-c to stop)`);
|
||||
|
||||
while (true) {
|
||||
const rants = await api.rants("recent", 20);
|
||||
for (const rant of rants.reverse()) {
|
||||
if (seen.has(rant.id)) continue;
|
||||
seen.add(rant.id);
|
||||
const text = rant.text.replace(/\n/g, " ").slice(0, 80);
|
||||
console.log(`[${rant.id}] ${rant.user_username}: ${text}`);
|
||||
if (keywords.length && api.auth.token_key) {
|
||||
const lowered = rant.text.toLowerCase();
|
||||
if (keywords.some((word) => lowered.includes(word))) {
|
||||
await api.voteRant(rant.id, 1);
|
||||
console.log(` upvoted ${rant.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
await sleep(pollSeconds * 1000);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from client import from_env
|
||||
|
||||
POLL_SECONDS = int(os.environ.get("DEVRANT_POLL_SECONDS", "30"))
|
||||
UPVOTE_KEYWORDS = [
|
||||
word.strip().lower()
|
||||
for word in os.environ.get("DEVRANT_UPVOTE_KEYWORDS", "").split(",")
|
||||
if word.strip()
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
api = from_env()
|
||||
if api.username and api.password:
|
||||
api.login()
|
||||
seen: set[int] = set()
|
||||
print(f"watching feed every {POLL_SECONDS}s (ctrl-c to stop)")
|
||||
while True:
|
||||
for rant in reversed(api.rants(sort="recent", limit=20)):
|
||||
rant_id = rant["id"]
|
||||
if rant_id in seen:
|
||||
continue
|
||||
seen.add(rant_id)
|
||||
text = rant["text"].replace("\n", " ")[:80]
|
||||
print(f"[{rant_id}] {rant['user_username']}: {text}")
|
||||
if UPVOTE_KEYWORDS and api.auth:
|
||||
lowered = rant["text"].lower()
|
||||
if any(word in lowered for word in UPVOTE_KEYWORDS):
|
||||
api.vote_rant(rant_id, 1)
|
||||
print(f" upvoted {rant_id}")
|
||||
time.sleep(POLL_SECONDS)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { fromEnv } from "./client.mjs";
|
||||
|
||||
const text = process.argv[2];
|
||||
const tags = process.argv[3] || "";
|
||||
|
||||
if (!text) {
|
||||
console.log("usage: DEVRANT_USERNAME=.. DEVRANT_PASSWORD=.. node post_rant.mjs <text> [tags]");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const api = fromEnv();
|
||||
await api.login();
|
||||
const result = await api.postRant(text, tags);
|
||||
if (result.success) {
|
||||
console.log(`posted rant ${result.rant_id}`);
|
||||
} else {
|
||||
console.log(`failed: ${result.error}`);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import sys
|
||||
|
||||
from client import from_env
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if len(sys.argv) < 2:
|
||||
print("usage: DEVRANT_USERNAME=.. DEVRANT_PASSWORD=.. python post_rant.py <text> [tags]")
|
||||
raise SystemExit(1)
|
||||
text = sys.argv[1]
|
||||
tags = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
api = from_env()
|
||||
api.login()
|
||||
result = api.post_rant(text, tags)
|
||||
if result.get("success"):
|
||||
print(f"posted rant {result['rant_id']}")
|
||||
else:
|
||||
print(f"failed: {result.get('error')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,77 @@
|
||||
// retoor <retoor@molodetz.nl>
|
||||
|
||||
import { DevRant } from "./client.mjs";
|
||||
|
||||
const base = process.env.DEVRANT_BASE || "http://localhost:10500";
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function check(label, condition, detail = "") {
|
||||
if (condition) {
|
||||
passed += 1;
|
||||
console.log(` PASS ${label}`);
|
||||
} else {
|
||||
failed += 1;
|
||||
console.log(` FAIL ${label} ${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
const suffix = String(Math.floor(Date.now() / 1000));
|
||||
const username = `dr_smoke_${suffix}`;
|
||||
const api = new DevRant(base, username, "secret6");
|
||||
|
||||
const reg = await api.register(`${username}@example.test`);
|
||||
check("register", reg.success, JSON.stringify(reg));
|
||||
|
||||
const login = await new DevRant(base, username, "secret6").login();
|
||||
check("login returns token", Boolean(login.token_key));
|
||||
await api.login();
|
||||
|
||||
const posted = await api.postRant("Smoke test rant about Python", "python,smoke,devrant");
|
||||
check("post rant", posted.success);
|
||||
const rantId = posted.rant_id;
|
||||
|
||||
const feed = await api.rants("recent", 5);
|
||||
check("feed returns rants", feed.some((r) => r.id === rantId));
|
||||
|
||||
const detail = await api.rant(rantId);
|
||||
check("get rant tags round-trip", JSON.stringify(detail.rant.tags) === JSON.stringify(["python", "smoke", "devrant"]));
|
||||
check("get rant editable", detail.rant.editable === true);
|
||||
|
||||
check("comment", (await api.comment(rantId, "Nice rant!")).success);
|
||||
|
||||
const voted = await api.voteRant(rantId, 1);
|
||||
check("vote score", voted.rant.score === 1);
|
||||
check("vote state", voted.rant.vote_state === 1);
|
||||
|
||||
check("favorite", (await api.favorite(rantId)).success);
|
||||
check("unfavorite", (await api.unfavorite(rantId)).success);
|
||||
|
||||
const uid = await api.userId(username);
|
||||
check("get-user-id", Boolean(uid));
|
||||
|
||||
let profile = await api.profile(uid);
|
||||
check("profile score", profile.score === 1);
|
||||
check("profile counts", profile.content.counts.rants === 1);
|
||||
|
||||
await api.editProfile({ profile_about: "I build with Python", profile_github: "smoke" });
|
||||
profile = await api.profile(uid);
|
||||
check("edit-profile bio", profile.about === "I build with Python");
|
||||
check("skills derived from bio", profile.skills === "I build with Python");
|
||||
|
||||
const notifs = await api.notifs();
|
||||
check("notif-feed shape", "unread" in notifs && "items" in notifs);
|
||||
|
||||
check("delete rant", (await api.deleteRant(rantId)).success);
|
||||
|
||||
let badRejected = false;
|
||||
try {
|
||||
await new DevRant(base, username, "wrongpw").login();
|
||||
} catch {
|
||||
badRejected = true;
|
||||
}
|
||||
check("bad login rejected", badRejected);
|
||||
|
||||
console.log(`\n${passed} passed, ${failed} failed`);
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -0,0 +1,87 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
from client import DevRant
|
||||
|
||||
BASE = os.environ.get("DEVRANT_BASE", "http://localhost:10500")
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
|
||||
def check(label: str, condition: bool, detail: str = "") -> None:
|
||||
global passed, failed
|
||||
if condition:
|
||||
passed += 1
|
||||
print(f" PASS {label}")
|
||||
else:
|
||||
failed += 1
|
||||
print(f" FAIL {label} {detail}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
suffix = str(int(time.time()))
|
||||
username = f"dr_smoke_{suffix}"
|
||||
api = DevRant(BASE, username, "secret6")
|
||||
|
||||
reg = api.register(f"{username}@example.test")
|
||||
check("register", reg.get("success"), str(reg))
|
||||
|
||||
login = DevRant(BASE, username, "secret6").login()
|
||||
check("login returns token", bool(login.get("token_key")))
|
||||
api.login()
|
||||
|
||||
posted = api.post_rant("Smoke test rant about Python", "python,smoke,devrant")
|
||||
check("post rant", posted.get("success"))
|
||||
rant_id = posted.get("rant_id")
|
||||
|
||||
feed = api.rants(limit=5)
|
||||
check("feed returns rants", any(r["id"] == rant_id for r in feed))
|
||||
|
||||
detail = api.rant(rant_id)
|
||||
check("get rant tags round-trip", detail.get("rant", {}).get("tags") == ["python", "smoke", "devrant"])
|
||||
check("get rant editable", detail.get("rant", {}).get("editable") is True)
|
||||
|
||||
commented = api.comment(rant_id, "Nice rant!")
|
||||
check("comment", commented.get("success"))
|
||||
|
||||
voted = api.vote_rant(rant_id, 1)
|
||||
check("vote score", voted.get("rant", {}).get("score") == 1)
|
||||
check("vote state", voted.get("rant", {}).get("vote_state") == 1)
|
||||
|
||||
check("favorite", api.favorite(rant_id).get("success"))
|
||||
check("unfavorite", api.unfavorite(rant_id).get("success"))
|
||||
|
||||
uid = api.user_id(username)
|
||||
check("get-user-id", bool(uid))
|
||||
|
||||
profile = api.profile(uid)
|
||||
check("profile score", profile.get("score") == 1)
|
||||
check("profile counts", profile.get("content", {}).get("counts", {}).get("rants") == 1)
|
||||
|
||||
api.edit_profile(profile_about="I build with Python", profile_github="smoke")
|
||||
profile = api.profile(uid)
|
||||
check("edit-profile bio", profile.get("about") == "I build with Python")
|
||||
check("skills derived from bio", profile.get("skills") == "I build with Python")
|
||||
|
||||
notifs = api.notifs()
|
||||
check("notif-feed shape", "unread" in notifs and "items" in notifs)
|
||||
|
||||
deleted = api.delete_rant(rant_id)
|
||||
check("delete rant", deleted.get("success"))
|
||||
|
||||
bad = DevRant(BASE, username, "wrongpw")
|
||||
try:
|
||||
bad.login()
|
||||
check("bad login rejected", False)
|
||||
except RuntimeError:
|
||||
check("bad login rejected", True)
|
||||
|
||||
print(f"\n{passed} passed, {failed} failed")
|
||||
raise SystemExit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user