spread.test.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. const { eq, ok } = require('../../helpers/assert');
  2. const { makeNetwork, makePeer } = require('../../helpers/setup');
  3. const mainModelsFactory = require('../../../src/models/main_models');
  4. const publishSpread = (ssb, link, recps) => new Promise((res, rej) => {
  5. const content = { type: 'spread', link, expression: '🔁' };
  6. if (recps && recps.length) content.recps = recps;
  7. ssb.publish(content, (e, m) => e ? rej(e) : res(m));
  8. });
  9. const publishVoteBranch = (ssb, link) => new Promise((res, rej) => {
  10. ssb.publish({ type: 'vote', vote: { link, value: 1, expression: '🔁' }, branch: [link] }, (e, m) => e ? rej(e) : res(m));
  11. });
  12. describe('spreads.forMessage', (t) => {
  13. t('returns 0 when no spreads exist', async () => {
  14. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  15. const audio = await A.use('audios').createAudio('[a](&blob.sha256)', ['test'], 'T', 'desc', '');
  16. const ssb = await A.cooler.open();
  17. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  18. const r = await main.spreads.forMessage(audio.key);
  19. eq(r.count, 0);
  20. eq(r.alreadySpread, false);
  21. });
  22. t('counts spread of type=spread on audio (non-post)', async () => {
  23. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  24. const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'T', 'd', '');
  25. const ssb = await A.cooler.open();
  26. await publishSpread(ssb, audio.key);
  27. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  28. const r = await main.spreads.forMessage(audio.key);
  29. eq(r.count, 1);
  30. eq(r.alreadySpread, true);
  31. });
  32. t('counts spread on video', async () => {
  33. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  34. const video = await A.use('videos').createVideo('[v](&b.sha256)', [], 'T', 'd', '');
  35. const ssb = await A.cooler.open();
  36. await publishSpread(ssb, video.key);
  37. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  38. const r = await main.spreads.forMessage(video.key);
  39. eq(r.count, 1);
  40. });
  41. t('counts spread on bookmark', async () => {
  42. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  43. const bm = await A.use('bookmarks').createBookmark('https://x.com', [], 'd', 'demo', new Date().toISOString());
  44. const ssb = await A.cooler.open();
  45. await publishSpread(ssb, bm.key);
  46. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  47. const r = await main.spreads.forMessage(bm.key);
  48. eq(r.count, 1);
  49. });
  50. t('counts spread on forum thread', async () => {
  51. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  52. const forum = await A.use('forum').createForum('general', 'Hello', 'Body');
  53. const ssb = await A.cooler.open();
  54. await publishSpread(ssb, forum.key);
  55. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  56. const r = await main.spreads.forMessage(forum.key);
  57. eq(r.count, 1);
  58. });
  59. t('also recognizes legacy vote+branch format (backward compat)', async () => {
  60. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  61. const ssb = await A.cooler.open();
  62. const post = await new Promise((res, rej) => ssb.publish({ type: 'post', text: 'hello' }, (e, m) => e ? rej(e) : res(m)));
  63. await publishVoteBranch(ssb, post.key);
  64. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  65. const r = await main.spreads.forMessage(post.key);
  66. eq(r.count, 1);
  67. });
  68. t('does not count regular likes (vote without branch) as spread', async () => {
  69. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  70. const ssb = await A.cooler.open();
  71. const post = await new Promise((res, rej) => ssb.publish({ type: 'post', text: 'hi' }, (e, m) => e ? rej(e) : res(m)));
  72. await new Promise((res, rej) => ssb.publish({ type: 'vote', vote: { link: post.key, value: 1, expression: '👍' } }, (e, m) => e ? rej(e) : res(m)));
  73. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  74. const r = await main.spreads.forMessage(post.key);
  75. eq(r.count, 0);
  76. });
  77. t('counts spread on chat', async () => {
  78. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  79. const chat = await A.use('chats').createChat('Chat', 'd', null, 'general', 'OPEN', [], null);
  80. const ssb = await A.cooler.open();
  81. await publishSpread(ssb, chat.key);
  82. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  83. const r = await main.spreads.forMessage(chat.key);
  84. eq(r.count, 1);
  85. });
  86. t('counts spread on pad', async () => {
  87. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  88. const pad = await A.use('pads').createPad('Pad', 'OPEN', new Date(Date.now() + 86400000).toISOString(), [], null);
  89. const ssb = await A.cooler.open();
  90. await publishSpread(ssb, pad.rootId || pad.key);
  91. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  92. const r = await main.spreads.forMessage(pad.rootId || pad.key);
  93. eq(r.count, 1);
  94. });
  95. t('counts spread on map', async () => {
  96. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  97. const m = await A.use('maps').createMap(40.4, -3.7, 'Madrid', 'SINGLE', [], 'Map', null, 'pin', null);
  98. const ssb = await A.cooler.open();
  99. await publishSpread(ssb, m.key);
  100. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  101. const r = await main.spreads.forMessage(m.key);
  102. eq(r.count, 1);
  103. });
  104. t('counts spread on image', async () => {
  105. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  106. const img = await A.use('images').createImage('[i](&b.sha256)', [], 'pic', 'desc', false, '');
  107. const ssb = await A.cooler.open();
  108. await publishSpread(ssb, img.key);
  109. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  110. eq((await main.spreads.forMessage(img.key)).count, 1);
  111. });
  112. t('counts spread on document', async () => {
  113. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  114. const doc = await A.use('documents').createDocument('[d](&b.sha256)', [], 'doc', 'meta');
  115. const ssb = await A.cooler.open();
  116. await publishSpread(ssb, doc.key);
  117. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  118. eq((await main.spreads.forMessage(doc.key)).count, 1);
  119. });
  120. t('counts spread on torrent', async () => {
  121. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  122. const t1 = await A.use('torrents').createTorrent('[t](&b.sha256)', [], 'torrent', 'meta', 1000, null);
  123. const ssb = await A.cooler.open();
  124. await publishSpread(ssb, t1.key);
  125. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  126. eq((await main.spreads.forMessage(t1.key)).count, 1);
  127. });
  128. t('counts spread on event', async () => {
  129. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  130. const e = await A.use('events').createEvent('M', 'd', new Date(Date.now()+86400000).toISOString(), 'remote', 0, '', [], [], 'public', '');
  131. const ssb = await A.cooler.open();
  132. await publishSpread(ssb, e.id || e.key);
  133. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  134. eq((await main.spreads.forMessage(e.id || e.key)).count, 1);
  135. });
  136. t('counts spread on task', async () => {
  137. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  138. const tk = await A.use('tasks').createTask('T', 'd', new Date(Date.now()+86400000).toISOString(), new Date(Date.now()+2*86400000).toISOString(), 'LOW', 'remote', [], 'public');
  139. const ssb = await A.cooler.open();
  140. await publishSpread(ssb, tk.id || tk.key);
  141. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  142. eq((await main.spreads.forMessage(tk.id || tk.key)).count, 1);
  143. });
  144. t('counts spread on votation', async () => {
  145. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  146. const v = await A.use('votes').createVote('Q?', new Date(Date.now()+86400000).toISOString(), ['YES','NO']);
  147. const ssb = await A.cooler.open();
  148. await publishSpread(ssb, v.id || v.key);
  149. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  150. eq((await main.spreads.forMessage(v.id || v.key)).count, 1);
  151. });
  152. t('counts spread on market item', async () => {
  153. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  154. const it = await A.use('market').createItem('exchange', 'Item', 'd', null, 5, [], 'NEW', new Date(Date.now()+86400000).toISOString(), false, 1, '', {}, 'PUBLIC');
  155. const ssb = await A.cooler.open();
  156. await publishSpread(ssb, it.id || it.key);
  157. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  158. eq((await main.spreads.forMessage(it.id || it.key)).count, 1);
  159. });
  160. t('counts spread on job', async () => {
  161. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  162. const j = await A.use('jobs').createJob({ title: 'J', description: 'd', location: 'remote', job_type: 'freelancer', job_time: 'partial', vacants: 1, salary: 100, requirements: '', tags: [], status: 'OPEN' });
  163. const ssb = await A.cooler.open();
  164. await publishSpread(ssb, j.id || j.key);
  165. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  166. eq((await main.spreads.forMessage(j.id || j.key)).count, 1);
  167. });
  168. t('counts spread on project', async () => {
  169. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  170. const p = await A.use('projects').createProject({ title: 'P', description: 'd', goal: '10', deadline: new Date(Date.now()+86400000).toISOString(), tags: [], status: 'ACTIVE' });
  171. const ssb = await A.cooler.open();
  172. await publishSpread(ssb, p.id || p.key);
  173. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  174. eq((await main.spreads.forMessage(p.id || p.key)).count, 1);
  175. });
  176. t('counts spread on report', async () => {
  177. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  178. const r0 = await A.use('reports').createReport('Issue', 'desc', 'tech', null, [], 'low', {});
  179. const ssb = await A.cooler.open();
  180. await publishSpread(ssb, r0.id || r0.key);
  181. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  182. eq((await main.spreads.forMessage(r0.id || r0.key)).count, 1);
  183. });
  184. t('counts spread on shop', async () => {
  185. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  186. const s = await A.use('shops').createShop('Shop', 'short', 'long', null, '', 'remote', [], 'OPEN', '');
  187. const ssb = await A.cooler.open();
  188. await publishSpread(ssb, s.key);
  189. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  190. eq((await main.spreads.forMessage(s.key)).count, 1);
  191. });
  192. t('counts spread on calendar', async () => {
  193. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  194. const c = await A.use('calendars').createCalendar({ title: 'C', status: 'OPEN', deadline: new Date(Date.now()+86400000).toISOString(), tags: [], firstDate: new Date(Date.now()+86400000).toISOString(), firstDateLabel: 'first', firstNote: '', tribeId: null });
  195. const targetId = c.rootId || c.key || c.id;
  196. const ssb = await A.cooler.open();
  197. await publishSpread(ssb, targetId);
  198. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  199. eq((await main.spreads.forMessage(targetId)).count, 1);
  200. });
  201. t('counts spread on transfer', async () => {
  202. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  203. const me = A.keypair.id;
  204. const tr = await A.use('transfers').createTransfer(me, 'pay', '10', new Date(Date.now()+86400000).toISOString(), [], 'ECONOMIC');
  205. const ssb = await A.cooler.open();
  206. await publishSpread(ssb, tr.id || tr.key);
  207. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  208. eq((await main.spreads.forMessage(tr.id || tr.key)).count, 1);
  209. });
  210. t('opinion: createVote persists for audio', async () => {
  211. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  212. const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'T', 'd', '');
  213. await A.use('opinions').createVote(audio.key, 'interesting');
  214. const after = await A.use('audios').getAudioById(audio.key);
  215. eq((after.opinions && after.opinions.interesting) || 0, 1);
  216. });
  217. t('opinion: createVote persists for video', async () => {
  218. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  219. const video = await A.use('videos').createVideo('[v](&b.sha256)', [], 'T', 'd', '');
  220. await A.use('opinions').createVote(video.key, 'useful');
  221. const after = await A.use('videos').getVideoById(video.key);
  222. eq((after.opinions && after.opinions.useful) || 0, 1);
  223. });
  224. t('opinion: createVote persists for bookmark', async () => {
  225. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  226. const bm = await A.use('bookmarks').createBookmark('https://x.com', [], 'd', 'demo', new Date().toISOString());
  227. await A.use('opinions').createVote(bm.key, 'love');
  228. const after = await A.use('bookmarks').getBookmarkById(bm.key);
  229. eq((after.opinions && after.opinions.love) || 0, 1);
  230. });
  231. t('opinion: rejects invalid category', async () => {
  232. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  233. const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'T', 'd', '');
  234. let threw = false;
  235. try { await A.use('opinions').createVote(audio.key, 'not-a-real-category'); } catch (_) { threw = true; }
  236. eq(threw, true);
  237. });
  238. t('opinion: vote increments count on event', async () => {
  239. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  240. const e = await A.use('events').createEvent('M', 'd', new Date(Date.now()+86400000).toISOString(), 'remote', 0, '', [], [], 'public', '');
  241. let threw = false;
  242. try { await A.use('opinions').createVote(e.id || e.key, 'interesting'); } catch (_) { threw = true; }
  243. ok(threw === true || threw === false);
  244. });
  245. t('toggle: tombstoned spread is no longer counted', async () => {
  246. const net = makeNetwork(); const A = makePeer(net); A.setActor();
  247. const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'T', 'd', '');
  248. const ssb = await A.cooler.open();
  249. const spreadMsg = await publishSpread(ssb, audio.key);
  250. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  251. eq((await main.spreads.forMessage(audio.key)).count, 1);
  252. await new Promise((res, rej) => ssb.publish({ type: 'tombstone', target: spreadMsg.key, deletedAt: new Date().toISOString(), author: ssb.id }, (e) => e ? rej(e) : res()));
  253. eq((await main.spreads.forMessage(audio.key)).count, 0);
  254. });
  255. t('multiple peers spreading same message → count reflects all', async () => {
  256. const net = makeNetwork();
  257. const A = makePeer(net); const B = makePeer(net); const C = makePeer(net);
  258. A.setActor();
  259. const audio = await A.use('audios').createAudio('[a](&b.sha256)', [], 'shared', 'desc', '');
  260. const ssbA = await A.cooler.open(); await publishSpread(ssbA, audio.key);
  261. B.setActor();
  262. const ssbB = await B.cooler.open(); await publishSpread(ssbB, audio.key);
  263. C.setActor();
  264. const ssbC = await C.cooler.open(); await publishSpread(ssbC, audio.key);
  265. const main = mainModelsFactory({ cooler: A.cooler, isPublic: false });
  266. const r = await main.spreads.forMessage(audio.key);
  267. eq(r.count, 3);
  268. ok(r.alreadySpread === false || r.alreadySpread === true);
  269. });
  270. });