federation_test.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. import os
  2. import time
  3. from typing import List
  4. from typing import Tuple
  5. import requests
  6. from html2text import html2text
  7. from little_boxes.collection import parse_collection
  8. def resp2plaintext(resp):
  9. """Convert the body of a requests reponse to plain text in order to make basic assertions."""
  10. return html2text(resp.text)
  11. class Instance(object):
  12. """Test instance wrapper."""
  13. def __init__(self, name, host_url, docker_url=None):
  14. self.host_url = host_url
  15. self.docker_url = docker_url or host_url
  16. self._create_delay = 10
  17. with open(
  18. os.path.join(
  19. os.path.dirname(os.path.abspath(__file__)),
  20. f"fixtures/{name}/config/admin_api_key.key",
  21. )
  22. ) as f:
  23. api_key = f.read()
  24. self._auth_headers = {"Authorization": f"Bearer {api_key}"}
  25. def _do_req(self, url):
  26. """Used to parse collection."""
  27. url = url.replace(self.docker_url, self.host_url)
  28. resp = requests.get(url, headers={"Accept": "application/activity+json"})
  29. resp.raise_for_status()
  30. return resp.json()
  31. def _parse_collection(self, payload=None, url=None):
  32. """Parses a collection (go through all the pages)."""
  33. return parse_collection(url=url, payload=payload, fetcher=self._do_req)
  34. def ping(self):
  35. """Ensures the homepage is reachable."""
  36. resp = requests.get(f"{self.host_url}/")
  37. resp.raise_for_status()
  38. assert resp.status_code == 200
  39. def debug(self):
  40. """Returns the debug infos (number of items in the inbox/outbox."""
  41. resp = requests.get(
  42. f"{self.host_url}/api/debug",
  43. headers={**self._auth_headers, "Accept": "application/json"},
  44. )
  45. resp.raise_for_status()
  46. return resp.json()
  47. def drop_db(self):
  48. """Drops the MongoDB DB."""
  49. resp = requests.delete(
  50. f"{self.host_url}/api/debug",
  51. headers={**self._auth_headers, "Accept": "application/json"},
  52. )
  53. resp.raise_for_status()
  54. return resp.json()
  55. def block(self, actor_url) -> None:
  56. """Blocks an actor."""
  57. # Instance1 follows instance2
  58. resp = requests.post(
  59. f"{self.host_url}/api/block",
  60. params={"actor": actor_url},
  61. headers=self._auth_headers,
  62. )
  63. assert resp.status_code == 201
  64. # We need to wait for the Follow/Accept dance
  65. time.sleep(self._create_delay / 2)
  66. return resp.json().get("activity")
  67. def follow(self, instance: "Instance") -> str:
  68. """Follows another instance."""
  69. # Instance1 follows instance2
  70. resp = requests.post(
  71. f"{self.host_url}/api/follow",
  72. json={"actor": instance.docker_url},
  73. headers=self._auth_headers,
  74. )
  75. assert resp.status_code == 201
  76. # We need to wait for the Follow/Accept dance
  77. time.sleep(self._create_delay)
  78. return resp.json().get("activity")
  79. def new_note(self, content, reply=None) -> str:
  80. """Creates a new note."""
  81. params = {"content": content}
  82. if reply:
  83. params["reply"] = reply
  84. resp = requests.post(
  85. f"{self.host_url}/api/new_note", json=params, headers=self._auth_headers
  86. )
  87. assert resp.status_code == 201
  88. time.sleep(self._create_delay)
  89. return resp.json().get("activity")
  90. def boost(self, oid: str) -> str:
  91. """Creates an Announce activity."""
  92. resp = requests.post(
  93. f"{self.host_url}/api/boost", json={"id": oid}, headers=self._auth_headers
  94. )
  95. assert resp.status_code == 201
  96. time.sleep(self._create_delay)
  97. return resp.json().get("activity")
  98. def like(self, oid: str) -> str:
  99. """Creates a Like activity."""
  100. resp = requests.post(
  101. f"{self.host_url}/api/like", json={"id": oid}, headers=self._auth_headers
  102. )
  103. assert resp.status_code == 201
  104. time.sleep(self._create_delay)
  105. return resp.json().get("activity")
  106. def delete(self, oid: str) -> str:
  107. """Creates a Delete activity."""
  108. resp = requests.post(
  109. f"{self.host_url}/api/note/delete",
  110. json={"id": oid},
  111. headers=self._auth_headers,
  112. )
  113. assert resp.status_code == 201
  114. time.sleep(self._create_delay)
  115. return resp.json().get("activity")
  116. def undo(self, oid: str) -> str:
  117. """Creates a Undo activity."""
  118. resp = requests.post(
  119. f"{self.host_url}/api/undo", json={"id": oid}, headers=self._auth_headers
  120. )
  121. assert resp.status_code == 201
  122. # We need to wait for the Follow/Accept dance
  123. time.sleep(self._create_delay)
  124. return resp.json().get("activity")
  125. def followers(self) -> List[str]:
  126. """Parses the followers collection."""
  127. resp = requests.get(
  128. f"{self.host_url}/followers",
  129. headers={"Accept": "application/activity+json"},
  130. )
  131. resp.raise_for_status()
  132. data = resp.json()
  133. return self._parse_collection(payload=data)
  134. def following(self):
  135. """Parses the following collection."""
  136. resp = requests.get(
  137. f"{self.host_url}/following",
  138. headers={"Accept": "application/activity+json"},
  139. )
  140. resp.raise_for_status()
  141. data = resp.json()
  142. return self._parse_collection(payload=data)
  143. def outbox(self):
  144. """Returns the instance outbox."""
  145. resp = requests.get(
  146. f"{self.host_url}/following",
  147. headers={"Accept": "application/activity+json"},
  148. )
  149. resp.raise_for_status()
  150. return resp.json()
  151. def outbox_get(self, aid):
  152. """Fetches a specific item from the instance outbox."""
  153. resp = requests.get(
  154. aid.replace(self.docker_url, self.host_url),
  155. headers={"Accept": "application/activity+json"},
  156. )
  157. resp.raise_for_status()
  158. return resp.json()
  159. def stream_jsonfeed(self):
  160. """Returns the "stream"'s JSON feed."""
  161. resp = requests.get(
  162. f"{self.host_url}/api/stream",
  163. headers={**self._auth_headers, "Accept": "application/json"},
  164. )
  165. resp.raise_for_status()
  166. return resp.json()
  167. def _instances() -> Tuple[Instance, Instance]:
  168. """Initializes the client for the two test instances."""
  169. instance1 = Instance("instance1", "http://docker:5006", "http://instance1_web:5005")
  170. instance1.ping()
  171. instance2 = Instance("instance2", "http://docker:5007", "http://instance2_web:5005")
  172. instance2.ping()
  173. # Return the DB
  174. instance1.drop_db()
  175. instance2.drop_db()
  176. return instance1, instance2
  177. def test_follow() -> None:
  178. """instance1 follows instance2."""
  179. instance1, instance2 = _instances()
  180. # Instance1 follows instance2
  181. instance1.follow(instance2)
  182. instance1_debug = instance1.debug()
  183. assert instance1_debug["inbox"] == 1 # An Accept activity should be there
  184. assert instance1_debug["outbox"] == 1 # We've sent a Follow activity
  185. instance2_debug = instance2.debug()
  186. assert instance2_debug["inbox"] == 1 # An Follow activity should be there
  187. assert instance2_debug["outbox"] == 1 # We've sent a Accept activity
  188. assert instance2.followers() == [instance1.docker_url]
  189. assert instance1.following() == [instance2.docker_url]
  190. def test_follow_unfollow():
  191. """instance1 follows instance2, then unfollows it."""
  192. instance1, instance2 = _instances()
  193. # Instance1 follows instance2
  194. follow_id = instance1.follow(instance2)
  195. instance1_debug = instance1.debug()
  196. assert instance1_debug["inbox"] == 1 # An Accept activity should be there
  197. assert instance1_debug["outbox"] == 1 # We've sent a Follow activity
  198. instance2_debug = instance2.debug()
  199. assert instance2_debug["inbox"] == 1 # An Follow activity should be there
  200. assert instance2_debug["outbox"] == 1 # We've sent a Accept activity
  201. assert instance2.followers() == [instance1.docker_url]
  202. assert instance1.following() == [instance2.docker_url]
  203. instance1.undo(follow_id)
  204. assert instance2.followers() == []
  205. assert instance1.following() == []
  206. instance1_debug = instance1.debug()
  207. assert instance1_debug["inbox"] == 1 # An Accept activity should be there
  208. assert instance1_debug["outbox"] == 2 # We've sent a Follow and a Undo activity
  209. instance2_debug = instance2.debug()
  210. assert instance2_debug["inbox"] == 2 # An Follow and Undo activity should be there
  211. assert instance2_debug["outbox"] == 1 # We've sent a Accept activity
  212. def test_post_content():
  213. """Instances follow each other, and instance1 creates a note."""
  214. instance1, instance2 = _instances()
  215. # Instance1 follows instance2
  216. instance1.follow(instance2)
  217. instance2.follow(instance1)
  218. inbox_stream = instance2.stream_jsonfeed()
  219. assert len(inbox_stream["items"]) == 0
  220. create_id = instance1.new_note("hello")
  221. instance2_debug = instance2.debug()
  222. assert (
  223. instance2_debug["inbox"] == 3
  224. ) # An Follow, Accept and Create activity should be there
  225. assert instance2_debug["outbox"] == 2 # We've sent a Accept and a Follow activity
  226. # Ensure the post is visible in instance2's stream
  227. inbox_stream = instance2.stream_jsonfeed()
  228. assert len(inbox_stream["items"]) == 1
  229. assert inbox_stream["items"][0]["id"] == create_id
  230. def test_block_and_post_content():
  231. """Instances follow each other, instance2 blocks instance1, instance1 creates a new note."""
  232. instance1, instance2 = _instances()
  233. # Instance1 follows instance2
  234. instance1.follow(instance2)
  235. instance2.follow(instance1)
  236. inbox_stream = instance2.stream_jsonfeed()
  237. assert len(inbox_stream["items"]) == 0
  238. instance2.block(instance1.docker_url)
  239. instance1.new_note("hello")
  240. instance2_debug = instance2.debug()
  241. assert (
  242. instance2_debug["inbox"] == 2
  243. ) # An Follow, Accept activity should be there, Create should have been dropped
  244. assert (
  245. instance2_debug["outbox"] == 3
  246. ) # We've sent a Accept and a Follow activity + the Block activity
  247. # Ensure the post is not visible in instance2's stream
  248. inbox_stream = instance2.stream_jsonfeed()
  249. assert len(inbox_stream["items"]) == 0
  250. def test_post_content_and_delete():
  251. """Instances follow each other, instance1 creates a new note, then deletes it."""
  252. instance1, instance2 = _instances()
  253. # Instance1 follows instance2
  254. instance1.follow(instance2)
  255. instance2.follow(instance1)
  256. inbox_stream = instance2.stream_jsonfeed()
  257. assert len(inbox_stream["items"]) == 0
  258. create_id = instance1.new_note("hello")
  259. instance2_debug = instance2.debug()
  260. assert (
  261. instance2_debug["inbox"] == 3
  262. ) # An Follow, Accept and Create activity should be there
  263. assert instance2_debug["outbox"] == 2 # We've sent a Accept and a Follow activity
  264. # Ensure the post is visible in instance2's stream
  265. inbox_stream = instance2.stream_jsonfeed()
  266. assert len(inbox_stream["items"]) == 1
  267. assert inbox_stream["items"][0]["id"] == create_id
  268. instance1.delete(f"{create_id}/activity")
  269. instance2_debug = instance2.debug()
  270. assert (
  271. instance2_debug["inbox"] == 4
  272. ) # An Follow, Accept and Create and Delete activity should be there
  273. assert instance2_debug["outbox"] == 2 # We've sent a Accept and a Follow activity
  274. # Ensure the post has been delete from instance2's stream
  275. inbox_stream = instance2.stream_jsonfeed()
  276. assert len(inbox_stream["items"]) == 0
  277. def test_post_content_and_like():
  278. """Instances follow each other, instance1 creates a new note, instance2 likes it."""
  279. instance1, instance2 = _instances()
  280. # Instance1 follows instance2
  281. instance1.follow(instance2)
  282. instance2.follow(instance1)
  283. create_id = instance1.new_note("hello")
  284. # Ensure the post is visible in instance2's stream
  285. inbox_stream = instance2.stream_jsonfeed()
  286. assert len(inbox_stream["items"]) == 1
  287. assert inbox_stream["items"][0]["id"] == create_id
  288. # Now, instance2 like the note
  289. like_id = instance2.like(f"{create_id}/activity")
  290. instance1_debug = instance1.debug()
  291. assert instance1_debug["inbox"] == 3 # Follow, Accept and Like
  292. assert instance1_debug["outbox"] == 3 # Folllow, Accept, and Create
  293. note = instance1.outbox_get(f"{create_id}/activity")
  294. assert "likes" in note
  295. assert note["likes"]["totalItems"] == 1
  296. likes = instance1._parse_collection(url=note["likes"]["first"])
  297. assert len(likes) == 1
  298. assert likes[0]["id"] == like_id
  299. def test_post_content_and_like_unlike() -> None:
  300. """Instances follow each other, instance1 creates a new note, instance2 likes it, then unlikes it."""
  301. instance1, instance2 = _instances()
  302. # Instance1 follows instance2
  303. instance1.follow(instance2)
  304. instance2.follow(instance1)
  305. create_id = instance1.new_note("hello")
  306. # Ensure the post is visible in instance2's stream
  307. inbox_stream = instance2.stream_jsonfeed()
  308. assert len(inbox_stream["items"]) == 1
  309. assert inbox_stream["items"][0]["id"] == create_id
  310. # Now, instance2 like the note
  311. like_id = instance2.like(f"{create_id}/activity")
  312. instance1_debug = instance1.debug()
  313. assert instance1_debug["inbox"] == 3 # Follow, Accept and Like
  314. assert instance1_debug["outbox"] == 3 # Folllow, Accept, and Create
  315. note = instance1.outbox_get(f"{create_id}/activity")
  316. assert "likes" in note
  317. assert note["likes"]["totalItems"] == 1
  318. likes = instance1._parse_collection(url=note["likes"]["first"])
  319. assert len(likes) == 1
  320. assert likes[0]["id"] == like_id
  321. instance2.undo(like_id)
  322. instance1_debug = instance1.debug()
  323. assert instance1_debug["inbox"] == 4 # Follow, Accept and Like and Undo
  324. assert instance1_debug["outbox"] == 3 # Folllow, Accept, and Create
  325. note = instance1.outbox_get(f"{create_id}/activity")
  326. assert "likes" in note
  327. assert note["likes"]["totalItems"] == 0
  328. def test_post_content_and_boost() -> None:
  329. """Instances follow each other, instance1 creates a new note, instance2 "boost" it."""
  330. instance1, instance2 = _instances()
  331. # Instance1 follows instance2
  332. instance1.follow(instance2)
  333. instance2.follow(instance1)
  334. create_id = instance1.new_note("hello")
  335. # Ensure the post is visible in instance2's stream
  336. inbox_stream = instance2.stream_jsonfeed()
  337. assert len(inbox_stream["items"]) == 1
  338. assert inbox_stream["items"][0]["id"] == create_id
  339. # Now, instance2 like the note
  340. boost_id = instance2.boost(f"{create_id}/activity")
  341. instance1_debug = instance1.debug()
  342. assert instance1_debug["inbox"] == 3 # Follow, Accept and Announce
  343. assert instance1_debug["outbox"] == 3 # Folllow, Accept, and Create
  344. note = instance1.outbox_get(f"{create_id}/activity")
  345. assert "shares" in note
  346. assert note["shares"]["totalItems"] == 1
  347. shares = instance1._parse_collection(url=note["shares"]["first"])
  348. assert len(shares) == 1
  349. assert shares[0]["id"] == boost_id
  350. def test_post_content_and_boost_unboost() -> None:
  351. """Instances follow each other, instance1 creates a new note, instance2 "boost" it, then "unboost" it."""
  352. instance1, instance2 = _instances()
  353. # Instance1 follows instance2
  354. instance1.follow(instance2)
  355. instance2.follow(instance1)
  356. create_id = instance1.new_note("hello")
  357. # Ensure the post is visible in instance2's stream
  358. inbox_stream = instance2.stream_jsonfeed()
  359. assert len(inbox_stream["items"]) == 1
  360. assert inbox_stream["items"][0]["id"] == create_id
  361. # Now, instance2 like the note
  362. boost_id = instance2.boost(f"{create_id}/activity")
  363. instance1_debug = instance1.debug()
  364. assert instance1_debug["inbox"] == 3 # Follow, Accept and Announce
  365. assert instance1_debug["outbox"] == 3 # Folllow, Accept, and Create
  366. note = instance1.outbox_get(f"{create_id}/activity")
  367. assert "shares" in note
  368. assert note["shares"]["totalItems"] == 1
  369. shares = instance1._parse_collection(url=note["shares"]["first"])
  370. assert len(shares) == 1
  371. assert shares[0]["id"] == boost_id
  372. instance2.undo(boost_id)
  373. instance1_debug = instance1.debug()
  374. assert instance1_debug["inbox"] == 4 # Follow, Accept and Announce and Undo
  375. assert instance1_debug["outbox"] == 3 # Folllow, Accept, and Create
  376. note = instance1.outbox_get(f"{create_id}/activity")
  377. assert "shares" in note
  378. assert note["shares"]["totalItems"] == 0
  379. def test_post_content_and_post_reply() -> None:
  380. """Instances follow each other, instance1 creates a new note, instance2 replies to it."""
  381. instance1, instance2 = _instances()
  382. # Instance1 follows instance2
  383. instance1.follow(instance2)
  384. instance2.follow(instance1)
  385. inbox_stream = instance2.stream_jsonfeed()
  386. assert len(inbox_stream["items"]) == 0
  387. instance1_create_id = instance1.new_note("hello")
  388. instance2_debug = instance2.debug()
  389. assert (
  390. instance2_debug["inbox"] == 3
  391. ) # An Follow, Accept and Create activity should be there
  392. assert instance2_debug["outbox"] == 2 # We've sent a Accept and a Follow activity
  393. # Ensure the post is visible in instance2's stream
  394. instance2_inbox_stream = instance2.stream_jsonfeed()
  395. assert len(instance2_inbox_stream["items"]) == 1
  396. assert instance2_inbox_stream["items"][0]["id"] == instance1_create_id
  397. instance2_create_id = instance2.new_note(
  398. f"hey @instance1@{instance1.docker_url}",
  399. reply=f"{instance1_create_id}/activity",
  400. )
  401. instance2_debug = instance2.debug()
  402. assert (
  403. instance2_debug["inbox"] == 3
  404. ) # An Follow, Accept and Create activity should be there
  405. assert (
  406. instance2_debug["outbox"] == 3
  407. ) # We've sent a Accept and a Follow and a Create activity
  408. instance1_debug = instance1.debug()
  409. assert (
  410. instance1_debug["inbox"] == 3
  411. ) # An Follow, Accept and Create activity should be there
  412. assert (
  413. instance1_debug["outbox"] == 3
  414. ) # We've sent a Accept and a Follow and a Create activity
  415. instance1_inbox_stream = instance1.stream_jsonfeed()
  416. assert len(instance1_inbox_stream["items"]) == 1
  417. assert instance1_inbox_stream["items"][0]["id"] == instance2_create_id
  418. instance1_note = instance1.outbox_get(f"{instance1_create_id}/activity")
  419. assert "replies" in instance1_note
  420. assert instance1_note["replies"]["totalItems"] == 1
  421. replies = instance1._parse_collection(url=instance1_note["replies"]["first"])
  422. assert len(replies) == 1
  423. assert replies[0]["id"] == f"{instance2_create_id}/activity"
  424. def test_post_content_and_post_reply_and_delete() -> None:
  425. """Instances follow each other, instance1 creates a new note, instance2 replies to it, then deletes its reply."""
  426. instance1, instance2 = _instances()
  427. # Instance1 follows instance2
  428. instance1.follow(instance2)
  429. instance2.follow(instance1)
  430. inbox_stream = instance2.stream_jsonfeed()
  431. assert len(inbox_stream["items"]) == 0
  432. instance1_create_id = instance1.new_note("hello")
  433. instance2_debug = instance2.debug()
  434. assert (
  435. instance2_debug["inbox"] == 3
  436. ) # An Follow, Accept and Create activity should be there
  437. assert instance2_debug["outbox"] == 2 # We've sent a Accept and a Follow activity
  438. # Ensure the post is visible in instance2's stream
  439. instance2_inbox_stream = instance2.stream_jsonfeed()
  440. assert len(instance2_inbox_stream["items"]) == 1
  441. assert instance2_inbox_stream["items"][0]["id"] == instance1_create_id
  442. instance2_create_id = instance2.new_note(
  443. f"hey @instance1@{instance1.docker_url}",
  444. reply=f"{instance1_create_id}/activity",
  445. )
  446. instance2_debug = instance2.debug()
  447. assert (
  448. instance2_debug["inbox"] == 3
  449. ) # An Follow, Accept and Create activity should be there
  450. assert (
  451. instance2_debug["outbox"] == 3
  452. ) # We've sent a Accept and a Follow and a Create activity
  453. instance1_debug = instance1.debug()
  454. assert (
  455. instance1_debug["inbox"] == 3
  456. ) # An Follow, Accept and Create activity should be there
  457. assert (
  458. instance1_debug["outbox"] == 3
  459. ) # We've sent a Accept and a Follow and a Create activity
  460. instance1_inbox_stream = instance1.stream_jsonfeed()
  461. assert len(instance1_inbox_stream["items"]) == 1
  462. assert instance1_inbox_stream["items"][0]["id"] == instance2_create_id
  463. instance1_note = instance1.outbox_get(f"{instance1_create_id}/activity")
  464. assert "replies" in instance1_note
  465. assert instance1_note["replies"]["totalItems"] == 1
  466. instance2.delete(f"{instance2_create_id}/activity")
  467. instance1_debug = instance1.debug()
  468. assert (
  469. instance1_debug["inbox"] == 4
  470. ) # An Follow, Accept and Create and Delete activity should be there
  471. assert (
  472. instance1_debug["outbox"] == 3
  473. ) # We've sent a Accept and a Follow and a Create activity
  474. instance1_note = instance1.outbox_get(f"{instance1_create_id}/activity")
  475. assert "replies" in instance1_note
  476. assert instance1_note["replies"]["totalItems"] == 0