From 04a832e63f3755fd10ed895ba58c6a9658eca0a4 Mon Sep 17 00:00:00 2001 From: aspalding Date: Tue, 10 Oct 2023 14:50:13 -0500 Subject: [PATCH 01/28] super rough test --- spec/functional/inbox.spec.js | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 894a5a1..5ca34d2 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -175,6 +175,11 @@ describe('inbox', function () { .get('/u/mocked') .reply(200, { id: 'https://mocked.com/u/mocked', inbox: 'https://mocked.com/inbox/mocked' }) nock('https://mocked.com') + request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(vote) + .expect(200) .get('/s/abc123/shares') .reply(200, { id: 'https://mocked.com/s/abc123/shares', @@ -1510,4 +1515,85 @@ describe('inbox', function () { .expect(400) }) }) + + fdescribe('question', function () { + let question + beforeEach(function () { + question = { + '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Create', + id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3', + to: ['https://localhost/u/test'], + audience: ['as:Public'], + actor: 'https://localhost/u/test', + object: { + type: 'Question', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19', + attributedTo: 'https://localhost/u/test', + to: ['https://localhost/u/test'], + audience: ['as:Public'], + content: 'Say, did you finish reading that book I lent you?', + oneOf: [ + { + type: 'Note', + name: 'Yes', + replies: { + type: 'Collection', + totalItems: 0 + } + }, + { + type: 'Note', + name: 'No', + replies: { + type: 'Collection', + totalItems: 0 + } + } + ] + }, + shares: { + id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares', + type: 'OrderedCollection', + totalItems: 0, + first: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares?page=true' + + }, + likes: { + id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes', + type: 'OrderedCollection', + totalItems: 0, + first: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes?page=true' + } + } + }) + it('allows voting', async function () { + await apex.store.saveActivity(question) + let vote = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost/u/voter#votes/123/activity", + "to": "https://localhost/u/test", + "actor": "https://localhost/u/voter", + "type": "Create", + "object": { + "id": "https://localhost/u/voter#votes/123", + "type": "Note", + "name": "Yes", + "attributedTo": "https://localhost/u/voter", + "to": "https://localhost/u/test", + "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" + } + } + let req = request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(vote) + .expect(200) + + question_stored = await apex.store.getActivity(question.id) + yes_vote_tally = question_stored.object.oneOf.find((option) => option.name === 'Yes' ).replies.totalItems + + expect(yes_vote_tally).toBe(1); + }) + }) }) From 6d3cc9eef0bf5eff8183b9d1b14b73655c4c58e1 Mon Sep 17 00:00:00 2001 From: aspalding Date: Tue, 10 Oct 2023 16:56:14 -0500 Subject: [PATCH 02/28] verbose test output --- spec/helpers/reporter.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/helpers/reporter.js b/spec/helpers/reporter.js index a828849..704511e 100644 --- a/spec/helpers/reporter.js +++ b/spec/helpers/reporter.js @@ -4,7 +4,8 @@ const SpecReporter = require('jasmine-spec-reporter').SpecReporter jasmine.getEnv().clearReporters() // remove default reporter logs jasmine.getEnv().addReporter(new SpecReporter({ // add jasmine-spec-reporter spec: { - displayPending: true + displayPending: true, + displayStacktrace: 'pretty' }, summary: { displayDuration: false From f8c006eb1c88ed58558a498079d13ead26c82ca0 Mon Sep 17 00:00:00 2001 From: aspalding Date: Thu, 12 Oct 2023 15:00:04 -0500 Subject: [PATCH 03/28] track responses in collection --- net/activity.js | 12 ++++ spec/functional/inbox.spec.js | 117 +++++++++++++++++++++------------- 2 files changed, 84 insertions(+), 45 deletions(-) diff --git a/net/activity.js b/net/activity.js index 09b6a2e..a57c72f 100644 --- a/net/activity.js +++ b/net/activity.js @@ -189,7 +189,19 @@ module.exports = { } } break + case 'create': + let linkedQuestion = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') + if (linkedQuestion) { + const targetActivity = object + let targetActivityChoice = targetActivity.name[0].toLowerCase() + let chosenCollection = linkedQuestion.oneOf.find(({ name }) => name[0].toLowerCase() === targetActivityChoice) + const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) + toDo.push((async () => { + activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) + })()) + } } + Promise.all(toDo).then(() => { // configure event hook to be triggered after response sent resLocal.eventName = 'apex-inbox' diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 5ca34d2..9fedb70 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1516,59 +1516,89 @@ describe('inbox', function () { }) }) - fdescribe('question', function () { + describe('question', function () { + let activity let question - beforeEach(function () { + beforeEach(async function () { question = { - '@context': 'https://www.w3.org/ns/activitystreams', + type: 'Question', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19', + attributedTo: ['https://localhost/u/test'], + to: ['https://localhost/u/test'], + audience: ['as:Public'], + content: ['Say, did you finish reading that book I lent you?'], + votersCount: [0], + oneOf: [ + { + type: 'Note', + name: ['Yes'], + replies: { + type: 'Collection', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/Yes', + totalItems: [0] + } + }, + { + type: 'Note', + name: ['No'], + replies: { + type: 'Collection', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/No', + totalItems: [0] + } + } + ] + } + activity = { + '@context': ['https://www.w3.org/ns/activitystreams'], type: 'Create', id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3', to: ['https://localhost/u/test'], audience: ['as:Public'], - actor: 'https://localhost/u/test', - object: { - type: 'Question', - id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19', - attributedTo: 'https://localhost/u/test', - to: ['https://localhost/u/test'], - audience: ['as:Public'], - content: 'Say, did you finish reading that book I lent you?', - oneOf: [ - { - type: 'Note', - name: 'Yes', - replies: { - type: 'Collection', - totalItems: 0 - } - }, - { - type: 'Note', - name: 'No', - replies: { - type: 'Collection', - totalItems: 0 - } - } - ] - }, - shares: { + actor: ['https://localhost/u/test'], + object: [question], + shares: [{ id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares', type: 'OrderedCollection', - totalItems: 0, - first: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares?page=true' - - }, - likes: { + totalItems: [0], + first: ['https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares?page=true'] + }], + likes: [{ id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes', type: 'OrderedCollection', - totalItems: 0, - first: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes?page=true' + totalItems: [0], + first: ['https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes?page=true'] + }] + } + await apex.store.saveActivity(activity) + await apex.store.saveObject(question) + }) + fit('tracks responses in a collection', async function () { + let vote = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost/u/test#votes/123/activity", + "to": "https://localhost/u/test", + "actor": "https://localhost/u/test", + "type": "Create", + "object": { + "id": "https://localhost/u/test#votes/123", + "type": "Note", + "name": "Yes", + "attributedTo": "https://localhost/u/test", + "to": "https://localhost/u/test", + "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" } } + resp = await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(vote) + .expect(200) + + storedVote = await apex.store.getActivity(vote.id, true) + expect(storedVote._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/Yes') }) - it('allows voting', async function () { - await apex.store.saveActivity(question) + it('keeps a voterCount tally', async function () { let vote = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://localhost/u/voter#votes/123/activity", @@ -1584,16 +1614,13 @@ describe('inbox', function () { "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" } } - let req = request(app) + await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(vote) .expect(200) - question_stored = await apex.store.getActivity(question.id) - yes_vote_tally = question_stored.object.oneOf.find((option) => option.name === 'Yes' ).replies.totalItems - - expect(yes_vote_tally).toBe(1); + expect(question_stored.object.votersCount).toBe(1) }) }) }) From 46f4b5e1ff59cde1f688a254c57997ef597e8e0f Mon Sep 17 00:00:00 2001 From: aspalding Date: Thu, 12 Oct 2023 15:02:07 -0500 Subject: [PATCH 04/28] consistent vocabulary --- net/validators.js | 5 +++++ spec/functional/inbox.spec.js | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/net/validators.js b/net/validators.js index 570dba4..85c90df 100644 --- a/net/validators.js +++ b/net/validators.js @@ -349,6 +349,11 @@ function outboxActivityObject (req, res, next) { resLocal.object = follow } } + // if (obj.type.toLowerCase() === 'question') { + // needs to have any of or one of with something in it + // maybe dont do this + // maybe check if content is provided? + // } next() }).catch(err => { apex.logger.warn('Error resolving outbox activity object', err.message) diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 9fedb70..0a883da 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1573,8 +1573,8 @@ describe('inbox', function () { await apex.store.saveActivity(activity) await apex.store.saveObject(question) }) - fit('tracks responses in a collection', async function () { - let vote = { + fit('tracks replies in a collection', async function () { + let reply = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://localhost/u/test#votes/123/activity", "to": "https://localhost/u/test", @@ -1589,14 +1589,14 @@ describe('inbox', function () { "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" } } - resp = await request(app) + await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(vote) + .send(reply) .expect(200) - storedVote = await apex.store.getActivity(vote.id, true) - expect(storedVote._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/Yes') + storedReply = await apex.store.getActivity(reply.id, true) + expect(storedReply._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/Yes') }) it('keeps a voterCount tally', async function () { let vote = { From 410cb38a939f64ecb3059dd8a24fa137e0906089 Mon Sep 17 00:00:00 2001 From: aspalding Date: Thu, 12 Oct 2023 15:25:33 -0500 Subject: [PATCH 05/28] remove code committed in error --- spec/functional/inbox.spec.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 0a883da..d880cc2 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -175,11 +175,6 @@ describe('inbox', function () { .get('/u/mocked') .reply(200, { id: 'https://mocked.com/u/mocked', inbox: 'https://mocked.com/inbox/mocked' }) nock('https://mocked.com') - request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(vote) - .expect(200) .get('/s/abc123/shares') .reply(200, { id: 'https://mocked.com/s/abc123/shares', From a6b5d48a0dafd1046139bb0db21068d2e1c9f692 Mon Sep 17 00:00:00 2001 From: aspalding Date: Thu, 12 Oct 2023 17:05:24 -0500 Subject: [PATCH 06/28] start testing question colletions get updated --- spec/functional/inbox.spec.js | 40 ++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index d880cc2..bab353e 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1511,7 +1511,7 @@ describe('inbox', function () { }) }) - describe('question', function () { + fdescribe('question', function () { let activity let question beforeEach(async function () { @@ -1529,7 +1529,7 @@ describe('inbox', function () { name: ['Yes'], replies: { type: 'Collection', - id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/Yes', + id: 'https://localhost/u/test/c/Yes', totalItems: [0] } }, @@ -1538,7 +1538,7 @@ describe('inbox', function () { name: ['No'], replies: { type: 'Collection', - id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/No', + id: 'https://localhost/u/test/c/No', totalItems: [0] } } @@ -1568,7 +1568,7 @@ describe('inbox', function () { await apex.store.saveActivity(activity) await apex.store.saveObject(question) }) - fit('tracks replies in a collection', async function () { + it('tracks replies in a collection', async function () { let reply = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://localhost/u/test#votes/123/activity", @@ -1591,7 +1591,37 @@ describe('inbox', function () { .expect(200) storedReply = await apex.store.getActivity(reply.id, true) - expect(storedReply._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/Yes') + expect(storedReply._meta.collection).toContain('https://localhost/u/test/c/Yes') + }) + fit('the question replies collection is updated', async function () { + let reply = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost/u/test#votes/123/activity", + "to": "https://localhost/u/test", + "actor": "https://localhost/u/test", + "type": "Create", + "object": { + "id": "https://localhost/u/test#votes/123", + "type": "Note", + "name": "Yes", + "attributedTo": "https://localhost/u/test", + "to": "https://localhost/u/test", + "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" + } + } + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + + let storedQuestion = await apex.store.getObject(question.id, true) + console.log(JSON.stringify(storedQuestion)) + + let chosenCollection = storedQuestion.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') + console.log(chosenCollection.replies.totalItems[0]); + + expect(chosenCollection.replies.totalItems[0]).toBe(1) }) it('keeps a voterCount tally', async function () { let vote = { From 724f27bf88235f8f46693a1bea2798941ca9a7e4 Mon Sep 17 00:00:00 2001 From: aspalding Date: Fri, 13 Oct 2023 15:26:40 -0500 Subject: [PATCH 07/28] wip updating reply collection --- net/activity.js | 8 ++++++++ pub/utils.js | 2 +- spec/functional/inbox.spec.js | 4 ---- spec/helpers/test-utils.js | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/net/activity.js b/net/activity.js index a57c72f..5b20fab 100644 --- a/net/activity.js +++ b/net/activity.js @@ -198,6 +198,14 @@ module.exports = { const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) toDo.push((async () => { activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) + + // publish updated object with updated replies count + let updatedTarget = await apex.updateCollection(chosenCollectionId) + if (updatedTarget) { + resLocal.postWork.push(async () => { + return apex.publishUpdate(recipient, updatedTarget, actorId) + }) + } })()) } } diff --git a/pub/utils.js b/pub/utils.js index 7dcbd99..afb2336 100644 --- a/pub/utils.js +++ b/pub/utils.js @@ -4,7 +4,7 @@ const path = require('path') const jsonld = require('jsonld') const merge = require('deepmerge') const actorStreamNames = ['inbox', 'outbox', 'following', 'followers', 'liked', 'blocked', 'rejected', 'rejections'] -const activityStreamNames = ['shares', 'likes'] +const activityStreamNames = ['shares', 'likes', 'replies'] const audienceFields = ['to', 'bto', 'cc', 'bcc', 'audience'] module.exports = { diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index bab353e..86ea549 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1616,11 +1616,7 @@ describe('inbox', function () { .expect(200) let storedQuestion = await apex.store.getObject(question.id, true) - console.log(JSON.stringify(storedQuestion)) - let chosenCollection = storedQuestion.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') - console.log(chosenCollection.replies.totalItems[0]); - expect(chosenCollection.replies.totalItems[0]).toBe(1) }) it('keeps a voterCount tally', async function () { diff --git a/spec/helpers/test-utils.js b/spec/helpers/test-utils.js index c767d53..0a75cc9 100644 --- a/spec/helpers/test-utils.js +++ b/spec/helpers/test-utils.js @@ -18,6 +18,7 @@ global.initApex = async function initApex () { blocked: '/u/:actor/blocked', rejections: '/u/:actor/rejections', rejected: '/u/:actor/rejected', + replies: '', nodeinfo: '/nodeinfo' } const app = express() From 65106c8c60a3bd5f8a12541a70122131713d172e Mon Sep 17 00:00:00 2001 From: aspalding Date: Tue, 17 Oct 2023 10:06:59 -0500 Subject: [PATCH 08/28] update original object to reflect votes --- net/activity.js | 15 ++++++++------- pub/utils.js | 2 +- spec/functional/inbox.spec.js | 24 +++++++++++++----------- spec/helpers/test-utils.js | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/net/activity.js b/net/activity.js index 5b20fab..6ba3652 100644 --- a/net/activity.js +++ b/net/activity.js @@ -198,14 +198,15 @@ module.exports = { const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) toDo.push((async () => { activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) + let updatedCollection = await apex.getCollection(chosenCollectionId) + linkedQuestion.oneOf.find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection + let updatedObject = await apex.store.updateObject(linkedQuestion, actorId, true) - // publish updated object with updated replies count - let updatedTarget = await apex.updateCollection(chosenCollectionId) - if (updatedTarget) { - resLocal.postWork.push(async () => { - return apex.publishUpdate(recipient, updatedTarget, actorId) - }) - } + // if (updatedTarget) { + // resLocal.postWork.push(async () => { + // return apex.publishUpdate(recipient, updatedTarget, actorId) + // }) + // } })()) } } diff --git a/pub/utils.js b/pub/utils.js index afb2336..7dcbd99 100644 --- a/pub/utils.js +++ b/pub/utils.js @@ -4,7 +4,7 @@ const path = require('path') const jsonld = require('jsonld') const merge = require('deepmerge') const actorStreamNames = ['inbox', 'outbox', 'following', 'followers', 'liked', 'blocked', 'rejected', 'rejections'] -const activityStreamNames = ['shares', 'likes', 'replies'] +const activityStreamNames = ['shares', 'likes'] const audienceFields = ['to', 'bto', 'cc', 'bcc', 'audience'] module.exports = { diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 86ea549..8714542 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1523,13 +1523,14 @@ describe('inbox', function () { audience: ['as:Public'], content: ['Say, did you finish reading that book I lent you?'], votersCount: [0], - oneOf: [ + oneOf: [ // client with give us type and name, server will insert replies, validate in outbox activity, validators.outbox { type: 'Note', name: ['Yes'], replies: { type: 'Collection', - id: 'https://localhost/u/test/c/Yes', + // id: 'https://localhost/u/test/c/Yes', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes', // make sure url encoded? totalItems: [0] } }, @@ -1538,7 +1539,7 @@ describe('inbox', function () { name: ['No'], replies: { type: 'Collection', - id: 'https://localhost/u/test/c/No', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/No', // make randomly generated id instead of using option totalItems: [0] } } @@ -1568,15 +1569,15 @@ describe('inbox', function () { await apex.store.saveActivity(activity) await apex.store.saveObject(question) }) - it('tracks replies in a collection', async function () { + fit('tracks replies in a collection', async function () { let reply = { "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://localhost/u/test#votes/123/activity", + "id": "https://localhost/s/2131231", "to": "https://localhost/u/test", "actor": "https://localhost/u/test", "type": "Create", "object": { - "id": "https://localhost/u/test#votes/123", + "id": "https://localhost/o/2131231", "type": "Note", "name": "Yes", "attributedTo": "https://localhost/u/test", @@ -1591,7 +1592,7 @@ describe('inbox', function () { .expect(200) storedReply = await apex.store.getActivity(reply.id, true) - expect(storedReply._meta.collection).toContain('https://localhost/u/test/c/Yes') + expect(storedReply._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes') }) fit('the question replies collection is updated', async function () { let reply = { @@ -1619,18 +1620,18 @@ describe('inbox', function () { let chosenCollection = storedQuestion.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') expect(chosenCollection.replies.totalItems[0]).toBe(1) }) - it('keeps a voterCount tally', async function () { + fit('keeps a voterCount tally', async function () { let vote = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://localhost/u/voter#votes/123/activity", "to": "https://localhost/u/test", - "actor": "https://localhost/u/voter", + "actor": "https://localhost/u/test", "type": "Create", "object": { "id": "https://localhost/u/voter#votes/123", "type": "Note", "name": "Yes", - "attributedTo": "https://localhost/u/voter", + "attributedTo": "https://localhost/u/test", "to": "https://localhost/u/test", "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" } @@ -1640,7 +1641,8 @@ describe('inbox', function () { .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(vote) .expect(200) - question_stored = await apex.store.getActivity(question.id) + let question_stored = await apex.store.getObject(question.id) + console.log(JSON.stringify(question_stored)) expect(question_stored.object.votersCount).toBe(1) }) }) diff --git a/spec/helpers/test-utils.js b/spec/helpers/test-utils.js index 0a75cc9..fa0e009 100644 --- a/spec/helpers/test-utils.js +++ b/spec/helpers/test-utils.js @@ -18,7 +18,7 @@ global.initApex = async function initApex () { blocked: '/u/:actor/blocked', rejections: '/u/:actor/rejections', rejected: '/u/:actor/rejected', - replies: '', + votes: '/o/:question/c/:id', nodeinfo: '/nodeinfo' } const app = express() From 2590bb344d887801c53406f62b255a62e75c8147 Mon Sep 17 00:00:00 2001 From: aspalding Date: Tue, 17 Oct 2023 12:18:56 -0500 Subject: [PATCH 09/28] solve voterCount for the most simple test case --- net/activity.js | 1 + spec/functional/inbox.spec.js | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/net/activity.js b/net/activity.js index 6ba3652..932fc13 100644 --- a/net/activity.js +++ b/net/activity.js @@ -200,6 +200,7 @@ module.exports = { activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) let updatedCollection = await apex.getCollection(chosenCollectionId) linkedQuestion.oneOf.find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection + linkedQuestion.votersCount = [linkedQuestion.votersCount[0] + 1] let updatedObject = await apex.store.updateObject(linkedQuestion, actorId, true) // if (updatedTarget) { diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 8714542..16278b1 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1641,9 +1641,8 @@ describe('inbox', function () { .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(vote) .expect(200) - let question_stored = await apex.store.getObject(question.id) - console.log(JSON.stringify(question_stored)) - expect(question_stored.object.votersCount).toBe(1) + let questionStored = await apex.store.getObject(question.id) + expect(questionStored.votersCount[0]).toEqual(1) }) }) }) From 5d7ac605a75c2411930b721a63173effa28fd61c Mon Sep 17 00:00:00 2001 From: aspalding Date: Tue, 17 Oct 2023 17:08:57 -0500 Subject: [PATCH 10/28] write a test for and support anyOf --- net/activity.js | 22 ++++++--- spec/functional/inbox.spec.js | 84 ++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 47 deletions(-) diff --git a/net/activity.js b/net/activity.js index 932fc13..037ff65 100644 --- a/net/activity.js +++ b/net/activity.js @@ -194,18 +194,28 @@ module.exports = { if (linkedQuestion) { const targetActivity = object let targetActivityChoice = targetActivity.name[0].toLowerCase() - let chosenCollection = linkedQuestion.oneOf.find(({ name }) => name[0].toLowerCase() === targetActivityChoice) + let questionType + if (Object.hasOwn(linkedQuestion, 'oneOf')) { + questionType = 'oneOf' + } else if (Object.hasOwn(linkedQuestion, 'anyOf')) { + questionType = 'anyOf' + } + let chosenCollection = linkedQuestion[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) toDo.push((async () => { + let actorHasVoted = activity._meta.collection.some((obj) => { + return obj.includes(linkedQuestion.id) + }) activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) let updatedCollection = await apex.getCollection(chosenCollectionId) - linkedQuestion.oneOf.find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection - linkedQuestion.votersCount = [linkedQuestion.votersCount[0] + 1] - let updatedObject = await apex.store.updateObject(linkedQuestion, actorId, true) - + if (updatedCollection && !actorHasVoted){ + linkedQuestion.votersCount = [linkedQuestion.votersCount[0] + 1] + } + linkedQuestion[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection + let updatedQuestion = await apex.store.updateObject(linkedQuestion, actorId, true) // if (updatedTarget) { // resLocal.postWork.push(async () => { - // return apex.publishUpdate(recipient, updatedTarget, actorId) + // return apex.publishUpdate(recipient, updatedQuestion, actorId) // }) // } })()) diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 16278b1..00232e8 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1514,6 +1514,7 @@ describe('inbox', function () { fdescribe('question', function () { let activity let question + let reply beforeEach(async function () { question = { type: 'Question', @@ -1523,14 +1524,14 @@ describe('inbox', function () { audience: ['as:Public'], content: ['Say, did you finish reading that book I lent you?'], votersCount: [0], - oneOf: [ // client with give us type and name, server will insert replies, validate in outbox activity, validators.outbox + oneOf: [ { type: 'Note', name: ['Yes'], replies: { type: 'Collection', // id: 'https://localhost/u/test/c/Yes', - id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes', // make sure url encoded? + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes', totalItems: [0] } }, @@ -1539,7 +1540,7 @@ describe('inbox', function () { name: ['No'], replies: { type: 'Collection', - id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/No', // make randomly generated id instead of using option + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/No', totalItems: [0] } } @@ -1566,11 +1567,7 @@ describe('inbox', function () { first: ['https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes?page=true'] }] } - await apex.store.saveActivity(activity) - await apex.store.saveObject(question) - }) - fit('tracks replies in a collection', async function () { - let reply = { + reply = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://localhost/s/2131231", "to": "https://localhost/u/test", @@ -1585,6 +1582,10 @@ describe('inbox', function () { "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" } } + await apex.store.saveActivity(activity) + await apex.store.saveObject(question) + }) + it('tracks replies in a collection', async function () { await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') @@ -1594,22 +1595,7 @@ describe('inbox', function () { storedReply = await apex.store.getActivity(reply.id, true) expect(storedReply._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes') }) - fit('the question replies collection is updated', async function () { - let reply = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://localhost/u/test#votes/123/activity", - "to": "https://localhost/u/test", - "actor": "https://localhost/u/test", - "type": "Create", - "object": { - "id": "https://localhost/u/test#votes/123", - "type": "Note", - "name": "Yes", - "attributedTo": "https://localhost/u/test", - "to": "https://localhost/u/test", - "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" - } - } + it('the question replies collection is updated', async function () { await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') @@ -1620,29 +1606,45 @@ describe('inbox', function () { let chosenCollection = storedQuestion.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') expect(chosenCollection.replies.totalItems[0]).toBe(1) }) - fit('keeps a voterCount tally', async function () { - let vote = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://localhost/u/voter#votes/123/activity", - "to": "https://localhost/u/test", - "actor": "https://localhost/u/test", - "type": "Create", - "object": { - "id": "https://localhost/u/voter#votes/123", - "type": "Note", - "name": "Yes", - "attributedTo": "https://localhost/u/test", - "to": "https://localhost/u/test", - "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" - } - } + it('keeps a voterCount tally', async function () { + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + let questionStored = await apex.store.getObject(question.id) + expect(questionStored.votersCount[0]).toEqual(1) + }) + it('anyOf property allows a user to vote for multiple choices', async function () { + question.anyOf = question.oneOf + delete question.oneOf + await apex.store.updateObject(question, 'test', true) + + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + reply.object.name = 'No' await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(vote) + .send(reply) .expect(200) + + let storedQuestion = await apex.store.getObject(question.id, true) + let yesCollection = storedQuestion.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') + expect(yesCollection.replies.totalItems[0]).toBe(1) + let noCollection = storedQuestion.anyOf.find(({ name }) => name[0].toLowerCase() === 'no') + expect(noCollection.replies.totalItems[0]).toBe(1) let questionStored = await apex.store.getObject(question.id) expect(questionStored.votersCount[0]).toEqual(1) }) + it('publishes the results') + describe('validations', function() { + it('wont allow a vote to a closed poll') + it('prevents the same user from voting for the same choice twice') + it('oneOf prevents the same user from voting for multiple choices') + }) }) }) From 6f40b7659e777e10c9603b9899034901116666e0 Mon Sep 17 00:00:00 2001 From: aspalding Date: Wed, 18 Oct 2023 12:37:48 -0500 Subject: [PATCH 11/28] test and implementation for publishing results --- net/activity.js | 10 +++++----- spec/functional/inbox.spec.js | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/net/activity.js b/net/activity.js index 037ff65..184eec6 100644 --- a/net/activity.js +++ b/net/activity.js @@ -213,11 +213,11 @@ module.exports = { } linkedQuestion[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection let updatedQuestion = await apex.store.updateObject(linkedQuestion, actorId, true) - // if (updatedTarget) { - // resLocal.postWork.push(async () => { - // return apex.publishUpdate(recipient, updatedQuestion, actorId) - // }) - // } + if (updatedQuestion) { + resLocal.postWork.push(async () => { + return apex.publishUpdate(recipient, updatedQuestion, actorId) + }) + } })()) } } diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 00232e8..87fef03 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1640,7 +1640,26 @@ describe('inbox', function () { let questionStored = await apex.store.getObject(question.id) expect(questionStored.votersCount[0]).toEqual(1) }) - it('publishes the results') + it('publishes the results', async function () { + let addrSpy = spyOn(apex, 'address').and.callFake(async () => ['https://ignore.com/inbox/ignored']) + const requestValidated = new Promise(resolve => { + nock('https://mocked.com').post('/inbox/mocked') + .reply(200) + .on('request', async (req, interceptor, body) => { + const sentActivity = JSON.parse(body) + expect(sentActivity.object.votersCount).toEqual(1) + expect(sentActivity.object.oneOf.find(({ name }) => name.toLowerCase() === 'yes').replies.totalItems).toEqual(1) + resolve() + }) + }) + addrSpy.and.callFake(async () => ['https://mocked.com/inbox/mocked']) + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/activity+json') + .send(reply) + .expect(200) + await requestValidated + }) describe('validations', function() { it('wont allow a vote to a closed poll') it('prevents the same user from voting for the same choice twice') From ae063a1189a7fe3a4292068b62f34f3f48829b1f Mon Sep 17 00:00:00 2001 From: aspalding Date: Wed, 18 Oct 2023 13:31:52 -0500 Subject: [PATCH 12/28] test to validate voting is blocked on closed polls --- spec/functional/inbox.spec.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 87fef03..bf39690 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1661,7 +1661,18 @@ describe('inbox', function () { await requestValidated }) describe('validations', function() { - it('wont allow a vote to a closed poll') + it('wont allow a vote to a closed poll', async function () { + let closedDate = new Date() + closedDate.setDate(closedDate.getDate() - 1) + question.endTime = closedDate + await apex.store.updateObject(question, 'test', true) + + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(403) + }) it('prevents the same user from voting for the same choice twice') it('oneOf prevents the same user from voting for multiple choices') }) From 8103fc0ad93ba3b95a238f3db986c7a1fccb82db Mon Sep 17 00:00:00 2001 From: aspalding Date: Wed, 18 Oct 2023 16:09:29 -0500 Subject: [PATCH 13/28] adjust order so linked property is available for more validations --- net/activity.js | 3 --- net/index.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/net/activity.js b/net/activity.js index 184eec6..0e59e60 100644 --- a/net/activity.js +++ b/net/activity.js @@ -338,9 +338,6 @@ module.exports = { resolveThread (req, res, next) { const apex = req.app.locals.apex const resLocal = res.locals.apex - if (!resLocal.activity) { - return next() - } apex.resolveReferences(req.body).then(refs => { resLocal.linked = refs next() diff --git a/net/index.js b/net/index.js index 2631ade..9dfac35 100644 --- a/net/index.js +++ b/net/index.js @@ -68,11 +68,11 @@ module.exports = { validators.jsonld, validators.targetActorWithMeta, security.verifySignature, + activity.resolveThread, validators.actor, validators.activityObject, validators.inboxActivity, activity.save, - activity.resolveThread, activity.inboxSideEffects, activity.forwardFromInbox, responders.status From 29ecafb7de325d143c5f0111ed9f6e02f2fc9198 Mon Sep 17 00:00:00 2001 From: aspalding Date: Wed, 18 Oct 2023 16:10:00 -0500 Subject: [PATCH 14/28] validate the polls are still open --- net/validators.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/net/validators.js b/net/validators.js index 85c90df..21d6970 100644 --- a/net/validators.js +++ b/net/validators.js @@ -136,6 +136,15 @@ function inboxActivity (req, res, next) { return next() } } + let linkedQuestion = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') + if (linkedQuestion) { + let now = new Date() + let pollEndTime = new Date(linkedQuestion.endTime) + if (now > pollEndTime) { + resLocal.status = 403 + next() + } + } tasks.push(apex.embedCollections(activity)) Promise.all(tasks).then(() => { apex.addMeta(req.body, 'collection', recipient.inbox[0]) @@ -349,11 +358,6 @@ function outboxActivityObject (req, res, next) { resLocal.object = follow } } - // if (obj.type.toLowerCase() === 'question') { - // needs to have any of or one of with something in it - // maybe dont do this - // maybe check if content is provided? - // } next() }).catch(err => { apex.logger.warn('Error resolving outbox activity object', err.message) From a403aa6ae0b2880a5fd34c1bbce9d9a88d88c84a Mon Sep 17 00:00:00 2001 From: aspalding Date: Wed, 18 Oct 2023 16:30:19 -0500 Subject: [PATCH 15/28] write test to prevent a user from voting for the same option twice --- spec/functional/inbox.spec.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index bf39690..3ea5d15 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1673,7 +1673,18 @@ describe('inbox', function () { .send(reply) .expect(403) }) - it('prevents the same user from voting for the same choice twice') + it('prevents the same user from voting for the same choice twice', async function () { + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(403) + }) it('oneOf prevents the same user from voting for multiple choices') }) }) From ed26bfeb4cd66233ca61d1fb9c0f83fce7e376a8 Mon Sep 17 00:00:00 2001 From: aspalding Date: Thu, 19 Oct 2023 16:04:15 -0500 Subject: [PATCH 16/28] refactor variable name --- net/activity.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/net/activity.js b/net/activity.js index 0e59e60..bb606d5 100644 --- a/net/activity.js +++ b/net/activity.js @@ -190,29 +190,30 @@ module.exports = { } break case 'create': - let linkedQuestion = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') - if (linkedQuestion) { + let question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') + if (question) { const targetActivity = object let targetActivityChoice = targetActivity.name[0].toLowerCase() let questionType - if (Object.hasOwn(linkedQuestion, 'oneOf')) { + if (Object.hasOwn(question, 'oneOf')) { questionType = 'oneOf' - } else if (Object.hasOwn(linkedQuestion, 'anyOf')) { + } else if (Object.hasOwn(question, 'anyOf')) { questionType = 'anyOf' } - let chosenCollection = linkedQuestion[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) + let chosenCollection = question[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) toDo.push((async () => { let actorHasVoted = activity._meta.collection.some((obj) => { - return obj.includes(linkedQuestion.id) + return obj.includes(question.id) }) activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) let updatedCollection = await apex.getCollection(chosenCollectionId) if (updatedCollection && !actorHasVoted){ - linkedQuestion.votersCount = [linkedQuestion.votersCount[0] + 1] + question.votersCount = [question.votersCount[0] + 1] } - linkedQuestion[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection - let updatedQuestion = await apex.store.updateObject(linkedQuestion, actorId, true) + question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection + + let updatedQuestion = await apex.store.updateObject(question, actorId, true) if (updatedQuestion) { resLocal.postWork.push(async () => { return apex.publishUpdate(recipient, updatedQuestion, actorId) From 4b060c73aaee827fd01f9559fca8939d379139c4 Mon Sep 17 00:00:00 2001 From: aspalding Date: Fri, 20 Oct 2023 10:05:23 -0500 Subject: [PATCH 17/28] test cleanup, put in basis of incomplete change --- net/activity.js | 9 ++++ spec/functional/inbox.spec.js | 78 ++++++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 23 deletions(-) diff --git a/net/activity.js b/net/activity.js index bb606d5..98da4ca 100644 --- a/net/activity.js +++ b/net/activity.js @@ -212,6 +212,15 @@ module.exports = { question.votersCount = [question.votersCount[0] + 1] } question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection + + let votes = [] + if (question._meta) { + votes = question._meta.votes + votes.push(activity.object[0].id) + } else { + votes.push(activity.object[0].id) + apex.addMeta(question, 'votes', votes) + } let updatedQuestion = await apex.store.updateObject(question, actorId, true) if (updatedQuestion) { diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 3ea5d15..d4524a2 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1511,7 +1511,7 @@ describe('inbox', function () { }) }) - fdescribe('question', function () { + describe('question', function () { let activity let question let reply @@ -1530,7 +1530,6 @@ describe('inbox', function () { name: ['Yes'], replies: { type: 'Collection', - // id: 'https://localhost/u/test/c/Yes', id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes', totalItems: [0] } @@ -1602,8 +1601,8 @@ describe('inbox', function () { .send(reply) .expect(200) - let storedQuestion = await apex.store.getObject(question.id, true) - let chosenCollection = storedQuestion.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') + let questionStored = await apex.store.getObject(question.id, true) + let chosenCollection = questionStored.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') expect(chosenCollection.replies.totalItems[0]).toBe(1) }) it('keeps a voterCount tally', async function () { @@ -1614,12 +1613,34 @@ describe('inbox', function () { .expect(200) let questionStored = await apex.store.getObject(question.id) expect(questionStored.votersCount[0]).toEqual(1) + await apex.createActor('voter', 'voter', 'voting user') + let anotherReply = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://localhost/s/2131232", + "to": "https://localhost/u/voter", + "actor": "https://localhost/u/test", + "type": "Create", + "object": { + "id": "https://localhost/o/2131232", + "type": "Note", + "name": "Yes", + "attributedTo": "https://localhost/u/voter", + "to": "https://localhost/u/test", + "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" + } + } + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(anotherReply) + .expect(200) + questionStored = await apex.store.getObject(question.id) + expect(questionStored.votersCount[0]).toEqual(2) }) it('anyOf property allows a user to vote for multiple choices', async function () { question.anyOf = question.oneOf delete question.oneOf await apex.store.updateObject(question, 'test', true) - await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') @@ -1631,13 +1652,12 @@ describe('inbox', function () { .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(reply) .expect(200) - - let storedQuestion = await apex.store.getObject(question.id, true) - let yesCollection = storedQuestion.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') + let questionStored = await apex.store.getObject(question.id, true) + let yesCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') expect(yesCollection.replies.totalItems[0]).toBe(1) - let noCollection = storedQuestion.anyOf.find(({ name }) => name[0].toLowerCase() === 'no') + let noCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'no') expect(noCollection.replies.totalItems[0]).toBe(1) - let questionStored = await apex.store.getObject(question.id) + questionStored = await apex.store.getObject(question.id) expect(questionStored.votersCount[0]).toEqual(1) }) it('publishes the results', async function () { @@ -1660,25 +1680,37 @@ describe('inbox', function () { .expect(200) await requestValidated }) + it('prevents the same user from voting for the same choice twice', async function () { + question.anyOf = question.oneOf + delete question.oneOf + await apex.store.updateObject(question, 'test', true) + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + let questionStored = await apex.store.getObject(question.id, true) + let yesCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') + expect(yesCollection.replies.totalItems[0]).toBe(1) + expect(questionStored._meta.votes[0]).toContain(reply.object.id) + reply.id = 'https://localhost/s/2131232' + reply.object.id = 'https://localhost/o/2131232' + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + questionStored = await apex.store.getObject(question.id, true) + yesCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') + expect(yesCollection.replies.totalItems[0]).toBe(1) + expect(questionStored._meta.votes[0]).not.toContain(reply.object.id) + }) describe('validations', function() { it('wont allow a vote to a closed poll', async function () { let closedDate = new Date() closedDate.setDate(closedDate.getDate() - 1) question.endTime = closedDate await apex.store.updateObject(question, 'test', true) - - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(403) - }) - it('prevents the same user from voting for the same choice twice', async function () { - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(200) await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') From 2f56463baed27929834f8ee3abf78cea8da122c6 Mon Sep 17 00:00:00 2001 From: aspalding Date: Mon, 23 Oct 2023 11:44:54 -0500 Subject: [PATCH 18/28] get test to fail as expected --- net/activity.js | 10 +++++++--- spec/functional/inbox.spec.js | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/net/activity.js b/net/activity.js index 98da4ca..6821b29 100644 --- a/net/activity.js +++ b/net/activity.js @@ -203,6 +203,7 @@ module.exports = { let chosenCollection = question[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) toDo.push((async () => { + question = await apex.store.getObject(question.id, true) let actorHasVoted = activity._meta.collection.some((obj) => { return obj.includes(question.id) }) @@ -213,15 +214,18 @@ module.exports = { } question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection - let votes = [] if (question._meta) { - votes = question._meta.votes - votes.push(activity.object[0].id) + question._meta.votes[0].push(activity.object[0].id) } else { + let votes = [] votes.push(activity.object[0].id) apex.addMeta(question, 'votes', votes) } + if (actorHasVoted) { + // if anyOf, check the votes to make sure they havent already voted an option, if they have, short circuit + // if oneOf, short circuit. + } let updatedQuestion = await apex.store.updateObject(question, actorId, true) if (updatedQuestion) { resLocal.postWork.push(async () => { diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index d4524a2..e346f66 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1511,7 +1511,7 @@ describe('inbox', function () { }) }) - describe('question', function () { + fdescribe('question', function () { let activity let question let reply @@ -1680,7 +1680,7 @@ describe('inbox', function () { .expect(200) await requestValidated }) - it('prevents the same user from voting for the same choice twice', async function () { + fit('prevents the same user from voting for the same choice twice', async function () { question.anyOf = question.oneOf delete question.oneOf await apex.store.updateObject(question, 'test', true) From 1ad5bbb1283b1fd14c100d1d4f04271e0d36af0a Mon Sep 17 00:00:00 2001 From: aspalding Date: Mon, 23 Oct 2023 14:27:56 -0500 Subject: [PATCH 19/28] cleanup and comment, to return --- net/activity.js | 20 +++++++++++++------- spec/functional/inbox.spec.js | 4 ++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/net/activity.js b/net/activity.js index 6821b29..095b3b1 100644 --- a/net/activity.js +++ b/net/activity.js @@ -192,9 +192,10 @@ module.exports = { case 'create': let question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') if (question) { + let questionType + let duplicateVote const targetActivity = object let targetActivityChoice = targetActivity.name[0].toLowerCase() - let questionType if (Object.hasOwn(question, 'oneOf')) { questionType = 'oneOf' } else if (Object.hasOwn(question, 'anyOf')) { @@ -207,13 +208,22 @@ module.exports = { let actorHasVoted = activity._meta.collection.some((obj) => { return obj.includes(question.id) }) - activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) + activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) // should this happen if vote isnt valid? let updatedCollection = await apex.getCollection(chosenCollectionId) if (updatedCollection && !actorHasVoted){ question.votersCount = [question.votersCount[0] + 1] } question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection + if (question._meta?.votes[0]) { + let countedVotes = await Promise.all(question._meta?.votes[0].map(async (id) => { + return await apex.store.getObject(id, true) + })) + duplicateVote = countedVotes.some(({attributedTo, name}) => { + return attributedTo[0] === activity.object[0].attributedTo[0] && name[0] === activity.object[0].name[0] + }) + } + if (question._meta) { question._meta.votes[0].push(activity.object[0].id) } else { @@ -221,11 +231,7 @@ module.exports = { votes.push(activity.object[0].id) apex.addMeta(question, 'votes', votes) } - - if (actorHasVoted) { - // if anyOf, check the votes to make sure they havent already voted an option, if they have, short circuit - // if oneOf, short circuit. - } + // if (!duplicateVote) { - this conditional results in other tests failing, while passing anyOf validation let updatedQuestion = await apex.store.updateObject(question, actorId, true) if (updatedQuestion) { resLocal.postWork.push(async () => { diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index e346f66..d4524a2 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1511,7 +1511,7 @@ describe('inbox', function () { }) }) - fdescribe('question', function () { + describe('question', function () { let activity let question let reply @@ -1680,7 +1680,7 @@ describe('inbox', function () { .expect(200) await requestValidated }) - fit('prevents the same user from voting for the same choice twice', async function () { + it('prevents the same user from voting for the same choice twice', async function () { question.anyOf = question.oneOf delete question.oneOf await apex.store.updateObject(question, 'test', true) From ba5d7243f5cff90e16e680897f14f24669fe3d75 Mon Sep 17 00:00:00 2001 From: aspalding Date: Fri, 27 Oct 2023 15:07:36 -0500 Subject: [PATCH 20/28] have resolve thread get meta --- net/activity.js | 5 +++-- pub/federation.js | 2 +- pub/object.js | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/net/activity.js b/net/activity.js index 095b3b1..64210ae 100644 --- a/net/activity.js +++ b/net/activity.js @@ -204,7 +204,6 @@ module.exports = { let chosenCollection = question[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) toDo.push((async () => { - question = await apex.store.getObject(question.id, true) let actorHasVoted = activity._meta.collection.some((obj) => { return obj.includes(question.id) }) @@ -231,13 +230,15 @@ module.exports = { votes.push(activity.object[0].id) apex.addMeta(question, 'votes', votes) } - // if (!duplicateVote) { - this conditional results in other tests failing, while passing anyOf validation + + // if (!duplicateVote) { //- this conditional results in other tests failing, while passing anyOf validation let updatedQuestion = await apex.store.updateObject(question, actorId, true) if (updatedQuestion) { resLocal.postWork.push(async () => { return apex.publishUpdate(recipient, updatedQuestion, actorId) }) } + // } })()) } } diff --git a/pub/federation.js b/pub/federation.js index b786544..c2e1252 100644 --- a/pub/federation.js +++ b/pub/federation.js @@ -44,7 +44,7 @@ const refProps = ['inReplyTo', 'object', 'target', 'tag'] async function resolveReferences (object, depth = 0) { const objectPromises = refProps.map(prop => object[prop]) .flat() // may have multiple tags to resolve - .map(o => this.resolveUnknown(o)) + .map(o => this.resolveUnknown(o, true)) .filter(p => p) const objects = (await Promise.allSettled(objectPromises)) .filter(r => r.status === 'fulfilled' && r.value) diff --git a/pub/object.js b/pub/object.js index 314e63e..96f8235 100644 --- a/pub/object.js +++ b/pub/object.js @@ -36,7 +36,7 @@ async function resolveObject (id, includeMeta, refresh, localOnly) { return object } -async function resolveUnknown (objectOrIRI) { +async function resolveUnknown (objectOrIRI, includeMeta) { let object if (!objectOrIRI) return null // For Link/Mention, we want to resolved the linked object @@ -45,9 +45,9 @@ async function resolveUnknown (objectOrIRI) { } // check if already cached if (this.isString(objectOrIRI)) { - object = await this.store.getActivity(objectOrIRI) + object = await this.store.getActivity(objectOrIRI, includeMeta) if (object) return object - object = await this.store.getObject(objectOrIRI) + object = await this.store.getObject(objectOrIRI, includeMeta) if (object) return object /* As local collections are not represented in the DB, instead being generated * on demand, they up getting requested via http below. Perhaps not the most efficient, From 6c4466178bdc3684b56c77c292a33e6919bace9f Mon Sep 17 00:00:00 2001 From: aspalding Date: Fri, 27 Oct 2023 15:36:45 -0500 Subject: [PATCH 21/28] change how voters are counted, remove validations from side effects --- net/activity.js | 27 ++++++++------------------- net/validators.js | 16 +++++++++++++--- spec/functional/inbox.spec.js | 23 +++++++++++++---------- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/net/activity.js b/net/activity.js index 64210ae..b7fdc5a 100644 --- a/net/activity.js +++ b/net/activity.js @@ -193,7 +193,6 @@ module.exports = { let question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') if (question) { let questionType - let duplicateVote const targetActivity = object let targetActivityChoice = targetActivity.name[0].toLowerCase() if (Object.hasOwn(question, 'oneOf')) { @@ -204,34 +203,24 @@ module.exports = { let chosenCollection = question[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) toDo.push((async () => { - let actorHasVoted = activity._meta.collection.some((obj) => { - return obj.includes(question.id) - }) activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) // should this happen if vote isnt valid? let updatedCollection = await apex.getCollection(chosenCollectionId) - if (updatedCollection && !actorHasVoted){ - question.votersCount = [question.votersCount[0] + 1] - } question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection - if (question._meta?.votes[0]) { - let countedVotes = await Promise.all(question._meta?.votes[0].map(async (id) => { - return await apex.store.getObject(id, true) - })) - duplicateVote = countedVotes.some(({attributedTo, name}) => { - return attributedTo[0] === activity.object[0].attributedTo[0] && name[0] === activity.object[0].name[0] - }) - } - if (question._meta) { question._meta.votes[0].push(activity.object[0].id) + if (!question._meta.voters[0].includes(activity.actor[0])) { + question._meta.voters[0].push(activity.actor[0]) + question.votersCount = question._meta.voters[0].length + } } else { - let votes = [] - votes.push(activity.object[0].id) + let votes = [activity.object[0].id] + let voters = [activity.actor[0]] + question.votersCount = 1 apex.addMeta(question, 'votes', votes) + apex.addMeta(question, 'voters', voters) } - // if (!duplicateVote) { //- this conditional results in other tests failing, while passing anyOf validation let updatedQuestion = await apex.store.updateObject(question, actorId, true) if (updatedQuestion) { resLocal.postWork.push(async () => { diff --git a/net/validators.js b/net/validators.js index 21d6970..ca09a6d 100644 --- a/net/validators.js +++ b/net/validators.js @@ -136,14 +136,24 @@ function inboxActivity (req, res, next) { return next() } } - let linkedQuestion = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') - if (linkedQuestion) { + let question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') + if (question) { + let questionType let now = new Date() - let pollEndTime = new Date(linkedQuestion.endTime) + let pollEndTime = new Date(question.endTime) if (now > pollEndTime) { resLocal.status = 403 next() } + if (Object.hasOwn(question, 'oneOf')) { + questionType = 'oneOf' + } else if (Object.hasOwn(question, 'anyOf')) { + questionType = 'anyOf' + } + // if () { + + // } + } tasks.push(apex.embedCollections(activity)) Promise.all(tasks).then(() => { diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index d4524a2..6a4dfb9 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1511,7 +1511,7 @@ describe('inbox', function () { }) }) - describe('question', function () { + fdescribe('question', function () { let activity let question let reply @@ -1605,20 +1605,21 @@ describe('inbox', function () { let chosenCollection = questionStored.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') expect(chosenCollection.replies.totalItems[0]).toBe(1) }) - it('keeps a voterCount tally', async function () { + fit('keeps a voterCount tally', async function () { await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(reply) .expect(200) - let questionStored = await apex.store.getObject(question.id) - expect(questionStored.votersCount[0]).toEqual(1) - await apex.createActor('voter', 'voter', 'voting user') + let questionStored = await apex.store.getObject(question.id, true) + expect(questionStored.votersCount).toEqual(1) + let anotherVoter = await apex.createActor('voter', 'voter', 'voting user') + await apex.store.saveObject(anotherVoter) let anotherReply = { "@context": "https://www.w3.org/ns/activitystreams", "id": "https://localhost/s/2131232", - "to": "https://localhost/u/voter", - "actor": "https://localhost/u/test", + "to": "https://localhost/u/test", + "actor": "https://localhost/u/voter", "type": "Create", "object": { "id": "https://localhost/o/2131232", @@ -1634,8 +1635,8 @@ describe('inbox', function () { .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(anotherReply) .expect(200) - questionStored = await apex.store.getObject(question.id) - expect(questionStored.votersCount[0]).toEqual(2) + questionStored = await apex.store.getObject(question.id, true) + expect(questionStored.votersCount).toEqual(2) }) it('anyOf property allows a user to vote for multiple choices', async function () { question.anyOf = question.oneOf @@ -1646,6 +1647,8 @@ describe('inbox', function () { .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(reply) .expect(200) + reply.id = 'https://localhost/s/2131232' + reply.object.id = 'https://localhost/o/2131232' reply.object.name = 'No' await request(app) .post('/inbox/test') @@ -1657,7 +1660,7 @@ describe('inbox', function () { expect(yesCollection.replies.totalItems[0]).toBe(1) let noCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'no') expect(noCollection.replies.totalItems[0]).toBe(1) - questionStored = await apex.store.getObject(question.id) + questionStored = await apex.store.getObject(question.id, true) expect(questionStored.votersCount[0]).toEqual(1) }) it('publishes the results', async function () { From 553eabf48b7066a9dea2b1fcffb7af30b537ea9e Mon Sep 17 00:00:00 2001 From: aspalding Date: Fri, 27 Oct 2023 15:59:09 -0500 Subject: [PATCH 22/28] implement oneOf, WIP anyOf --- net/activity.js | 1 + net/validators.js | 11 +++++------ spec/functional/inbox.spec.js | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/net/activity.js b/net/activity.js index b7fdc5a..d664d5a 100644 --- a/net/activity.js +++ b/net/activity.js @@ -216,6 +216,7 @@ module.exports = { } else { let votes = [activity.object[0].id] let voters = [activity.actor[0]] + let voteAndVoter = [{voter: activity.actor[0], vote: activity.object[0].id}] // replaces votes with this, then access in validation question.votersCount = 1 apex.addMeta(question, 'votes', votes) apex.addMeta(question, 'voters', voters) diff --git a/net/validators.js b/net/validators.js index ca09a6d..b7cb7c9 100644 --- a/net/validators.js +++ b/net/validators.js @@ -138,7 +138,6 @@ function inboxActivity (req, res, next) { } let question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') if (question) { - let questionType let now = new Date() let pollEndTime = new Date(question.endTime) if (now > pollEndTime) { @@ -146,13 +145,13 @@ function inboxActivity (req, res, next) { next() } if (Object.hasOwn(question, 'oneOf')) { - questionType = 'oneOf' + if (question._meta?.voters[0].includes(activity.actor[0])) { + resLocal.status = 403 + next() + } } else if (Object.hasOwn(question, 'anyOf')) { - questionType = 'anyOf' + // check whats up, need to be able to map a user with their choices } - // if () { - - // } } tasks.push(apex.embedCollections(activity)) diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 6a4dfb9..42d2faa 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1605,7 +1605,7 @@ describe('inbox', function () { let chosenCollection = questionStored.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') expect(chosenCollection.replies.totalItems[0]).toBe(1) }) - fit('keeps a voterCount tally', async function () { + it('keeps a voterCount tally', async function () { await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') @@ -1661,7 +1661,7 @@ describe('inbox', function () { let noCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'no') expect(noCollection.replies.totalItems[0]).toBe(1) questionStored = await apex.store.getObject(question.id, true) - expect(questionStored.votersCount[0]).toEqual(1) + expect(questionStored.votersCount).toEqual(1) }) it('publishes the results', async function () { let addrSpy = spyOn(apex, 'address').and.callFake(async () => ['https://ignore.com/inbox/ignored']) From d557f0ac740f0918cfdbe8047c22cb828ebafa30 Mon Sep 17 00:00:00 2001 From: aspalding Date: Mon, 30 Oct 2023 09:59:08 -0500 Subject: [PATCH 23/28] fix other validations --- net/activity.js | 12 +++++++- net/validators.js | 10 +++++- spec/functional/inbox.spec.js | 58 +++++++++++++++++++---------------- 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/net/activity.js b/net/activity.js index d664d5a..c79cf25 100644 --- a/net/activity.js +++ b/net/activity.js @@ -209,6 +209,11 @@ module.exports = { if (question._meta) { question._meta.votes[0].push(activity.object[0].id) + question._meta.voteAndVoter[0].push({ + voter: activity.actor[0], + vote: activity.object[0].id, + voteName: activity.object[0].name[0] + }) if (!question._meta.voters[0].includes(activity.actor[0])) { question._meta.voters[0].push(activity.actor[0]) question.votersCount = question._meta.voters[0].length @@ -216,10 +221,15 @@ module.exports = { } else { let votes = [activity.object[0].id] let voters = [activity.actor[0]] - let voteAndVoter = [{voter: activity.actor[0], vote: activity.object[0].id}] // replaces votes with this, then access in validation + let voteAndVoter = [{ + voter: activity.actor[0], + vote: activity.object[0].id, + voteName: activity.object[0].name[0] + }] // replaces votes with this, then access in validation question.votersCount = 1 apex.addMeta(question, 'votes', votes) apex.addMeta(question, 'voters', voters) + apex.addMeta(question, 'voteAndVoter', voteAndVoter) } let updatedQuestion = await apex.store.updateObject(question, actorId, true) diff --git a/net/validators.js b/net/validators.js index b7cb7c9..596e8e8 100644 --- a/net/validators.js +++ b/net/validators.js @@ -150,7 +150,15 @@ function inboxActivity (req, res, next) { next() } } else if (Object.hasOwn(question, 'anyOf')) { - // check whats up, need to be able to map a user with their choices + if (question._meta) { + let hasDuplicateVote = question._meta.voteAndVoter[0].some(({voter, voteName}) => { + return voter === activity.actor[0] && activity.object[0].name == voteName; + }) + if (hasDuplicateVote) { + resLocal.status = 403 + next() + } + } } } diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 42d2faa..2b0453a 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1683,31 +1683,6 @@ describe('inbox', function () { .expect(200) await requestValidated }) - it('prevents the same user from voting for the same choice twice', async function () { - question.anyOf = question.oneOf - delete question.oneOf - await apex.store.updateObject(question, 'test', true) - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(200) - let questionStored = await apex.store.getObject(question.id, true) - let yesCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') - expect(yesCollection.replies.totalItems[0]).toBe(1) - expect(questionStored._meta.votes[0]).toContain(reply.object.id) - reply.id = 'https://localhost/s/2131232' - reply.object.id = 'https://localhost/o/2131232' - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(200) - questionStored = await apex.store.getObject(question.id, true) - yesCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') - expect(yesCollection.replies.totalItems[0]).toBe(1) - expect(questionStored._meta.votes[0]).not.toContain(reply.object.id) - }) describe('validations', function() { it('wont allow a vote to a closed poll', async function () { let closedDate = new Date() @@ -1720,7 +1695,38 @@ describe('inbox', function () { .send(reply) .expect(403) }) - it('oneOf prevents the same user from voting for multiple choices') + it('prevents the same user from voting for the same choice twice', async function () { + question.anyOf = question.oneOf + delete question.oneOf + await apex.store.updateObject(question, 'test', true) + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + reply.id = 'https://localhost/s/2131232' + reply.object.id = 'https://localhost/o/2131232' + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(403) + }) + it('oneOf prevents the same user from voting for multiple choices', async function () { + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + reply.id = 'https://localhost/s/2131232' + reply.object.id = 'https://localhost/o/2131232' + reply.object.name = 'No' + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(403) + }) }) }) }) From 777848ffa004f6291c08c7a671070aca77333323 Mon Sep 17 00:00:00 2001 From: aspalding Date: Mon, 30 Oct 2023 10:18:50 -0500 Subject: [PATCH 24/28] refactor unneeded code --- net/activity.js | 15 +++------------ net/validators.js | 16 +++++++--------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/net/activity.js b/net/activity.js index c79cf25..decc47e 100644 --- a/net/activity.js +++ b/net/activity.js @@ -1,5 +1,6 @@ 'use strict' + // For collection display, store with objects resolved // updates also get their objects denormalized during validation const denormalizeObject = [ @@ -208,27 +209,17 @@ module.exports = { question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection if (question._meta) { - question._meta.votes[0].push(activity.object[0].id) question._meta.voteAndVoter[0].push({ voter: activity.actor[0], - vote: activity.object[0].id, voteName: activity.object[0].name[0] }) - if (!question._meta.voters[0].includes(activity.actor[0])) { - question._meta.voters[0].push(activity.actor[0]) - question.votersCount = question._meta.voters[0].length - } + question.votersCount = [...new Set(question._meta.voteAndVoter[0].map(obj => obj.voter))].length } else { - let votes = [activity.object[0].id] - let voters = [activity.actor[0]] let voteAndVoter = [{ voter: activity.actor[0], - vote: activity.object[0].id, voteName: activity.object[0].name[0] - }] // replaces votes with this, then access in validation + }] question.votersCount = 1 - apex.addMeta(question, 'votes', votes) - apex.addMeta(question, 'voters', voters) apex.addMeta(question, 'voteAndVoter', voteAndVoter) } diff --git a/net/validators.js b/net/validators.js index 596e8e8..c27aef1 100644 --- a/net/validators.js +++ b/net/validators.js @@ -145,19 +145,17 @@ function inboxActivity (req, res, next) { next() } if (Object.hasOwn(question, 'oneOf')) { - if (question._meta?.voters[0].includes(activity.actor[0])) { + if (question._meta?.voteAndVoter[0].map(obj => obj.voter).includes(activity.actor[0])) { resLocal.status = 403 next() } } else if (Object.hasOwn(question, 'anyOf')) { - if (question._meta) { - let hasDuplicateVote = question._meta.voteAndVoter[0].some(({voter, voteName}) => { - return voter === activity.actor[0] && activity.object[0].name == voteName; - }) - if (hasDuplicateVote) { - resLocal.status = 403 - next() - } + let hasDuplicateVote = question._meta?.voteAndVoter[0].some(({voter, voteName}) => { + return voter === activity.actor[0] && activity.object[0].name == voteName; + }) + if (hasDuplicateVote) { + resLocal.status = 403 + next() } } From 15e12b90fa9d0c13066874f71989bb863989db5c Mon Sep 17 00:00:00 2001 From: aspalding Date: Mon, 30 Oct 2023 10:21:48 -0500 Subject: [PATCH 25/28] cleanup white space, comments --- net/activity.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/net/activity.js b/net/activity.js index decc47e..c8d944e 100644 --- a/net/activity.js +++ b/net/activity.js @@ -1,6 +1,5 @@ 'use strict' - // For collection display, store with objects resolved // updates also get their objects denormalized during validation const denormalizeObject = [ @@ -204,10 +203,9 @@ module.exports = { let chosenCollection = question[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) toDo.push((async () => { - activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) // should this happen if vote isnt valid? + activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) let updatedCollection = await apex.getCollection(chosenCollectionId) question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection - if (question._meta) { question._meta.voteAndVoter[0].push({ voter: activity.actor[0], @@ -222,18 +220,15 @@ module.exports = { question.votersCount = 1 apex.addMeta(question, 'voteAndVoter', voteAndVoter) } - let updatedQuestion = await apex.store.updateObject(question, actorId, true) if (updatedQuestion) { resLocal.postWork.push(async () => { return apex.publishUpdate(recipient, updatedQuestion, actorId) }) } - // } })()) } } - Promise.all(toDo).then(() => { // configure event hook to be triggered after response sent resLocal.eventName = 'apex-inbox' From 5f209b48eabfe527f10c8ad2466f2c0b03d38236 Mon Sep 17 00:00:00 2001 From: aspalding Date: Mon, 30 Oct 2023 11:47:45 -0500 Subject: [PATCH 26/28] lint auto fixed --- net/activity.js | 70 +++---- net/validators.js | 11 +- spec/functional/inbox.spec.js | 370 +++++++++++++++++----------------- 3 files changed, 225 insertions(+), 226 deletions(-) diff --git a/net/activity.js b/net/activity.js index c8d944e..479a060 100644 --- a/net/activity.js +++ b/net/activity.js @@ -190,43 +190,43 @@ module.exports = { } break case 'create': - let question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') + const question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') if (question) { - let questionType - const targetActivity = object - let targetActivityChoice = targetActivity.name[0].toLowerCase() - if (Object.hasOwn(question, 'oneOf')) { - questionType = 'oneOf' - } else if (Object.hasOwn(question, 'anyOf')) { - questionType = 'anyOf' + let questionType + const targetActivity = object + const targetActivityChoice = targetActivity.name[0].toLowerCase() + if (Object.hasOwn(question, 'oneOf')) { + questionType = 'oneOf' + } else if (Object.hasOwn(question, 'anyOf')) { + questionType = 'anyOf' + } + const chosenCollection = question[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) + const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) + toDo.push((async () => { + activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) + const updatedCollection = await apex.getCollection(chosenCollectionId) + question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection + if (question._meta) { + question._meta.voteAndVoter[0].push({ + voter: activity.actor[0], + voteName: activity.object[0].name[0] + }) + question.votersCount = [...new Set(question._meta.voteAndVoter[0].map(obj => obj.voter))].length + } else { + const voteAndVoter = [{ + voter: activity.actor[0], + voteName: activity.object[0].name[0] + }] + question.votersCount = 1 + apex.addMeta(question, 'voteAndVoter', voteAndVoter) } - let chosenCollection = question[questionType].find(({ name }) => name[0].toLowerCase() === targetActivityChoice) - const chosenCollectionId = apex.objectIdFromValue(chosenCollection.replies) - toDo.push((async () => { - activity = await apex.store.updateActivityMeta(activity, 'collection', chosenCollectionId) - let updatedCollection = await apex.getCollection(chosenCollectionId) - question[questionType].find(({ replies }) => replies.id === chosenCollectionId).replies = updatedCollection - if (question._meta) { - question._meta.voteAndVoter[0].push({ - voter: activity.actor[0], - voteName: activity.object[0].name[0] - }) - question.votersCount = [...new Set(question._meta.voteAndVoter[0].map(obj => obj.voter))].length - } else { - let voteAndVoter = [{ - voter: activity.actor[0], - voteName: activity.object[0].name[0] - }] - question.votersCount = 1 - apex.addMeta(question, 'voteAndVoter', voteAndVoter) - } - let updatedQuestion = await apex.store.updateObject(question, actorId, true) - if (updatedQuestion) { - resLocal.postWork.push(async () => { - return apex.publishUpdate(recipient, updatedQuestion, actorId) - }) - } - })()) + const updatedQuestion = await apex.store.updateObject(question, actorId, true) + if (updatedQuestion) { + resLocal.postWork.push(async () => { + return apex.publishUpdate(recipient, updatedQuestion, actorId) + }) + } + })()) } } Promise.all(toDo).then(() => { diff --git a/net/validators.js b/net/validators.js index c27aef1..c0fbbe2 100644 --- a/net/validators.js +++ b/net/validators.js @@ -136,10 +136,10 @@ function inboxActivity (req, res, next) { return next() } } - let question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') + const question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') if (question) { - let now = new Date() - let pollEndTime = new Date(question.endTime) + const now = new Date() + const pollEndTime = new Date(question.endTime) if (now > pollEndTime) { resLocal.status = 403 next() @@ -150,15 +150,14 @@ function inboxActivity (req, res, next) { next() } } else if (Object.hasOwn(question, 'anyOf')) { - let hasDuplicateVote = question._meta?.voteAndVoter[0].some(({voter, voteName}) => { - return voter === activity.actor[0] && activity.object[0].name == voteName; + const hasDuplicateVote = question._meta?.voteAndVoter[0].some(({ voter, voteName }) => { + return voter === activity.actor[0] && activity.object[0].name == voteName }) if (hasDuplicateVote) { resLocal.status = 403 next() } } - } tasks.push(apex.embedCollections(activity)) Promise.all(tasks).then(() => { diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 2b0453a..6c01ef9 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1511,137 +1511,208 @@ describe('inbox', function () { }) }) - fdescribe('question', function () { - let activity - let question - let reply - beforeEach(async function () { - question = { - type: 'Question', - id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19', - attributedTo: ['https://localhost/u/test'], - to: ['https://localhost/u/test'], - audience: ['as:Public'], - content: ['Say, did you finish reading that book I lent you?'], - votersCount: [0], - oneOf: [ - { - type: 'Note', - name: ['Yes'], - replies: { - type: 'Collection', - id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes', - totalItems: [0] - } - }, - { - type: 'Note', - name: ['No'], - replies: { - type: 'Collection', - id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/No', - totalItems: [0] - } + fdescribe('question', function () { + let activity + let question + let reply + beforeEach(async function () { + question = { + type: 'Question', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19', + attributedTo: ['https://localhost/u/test'], + to: ['https://localhost/u/test'], + audience: ['as:Public'], + content: ['Say, did you finish reading that book I lent you?'], + votersCount: [0], + oneOf: [ + { + type: 'Note', + name: ['Yes'], + replies: { + type: 'Collection', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes', + totalItems: [0] + } + }, + { + type: 'Note', + name: ['No'], + replies: { + type: 'Collection', + id: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/No', + totalItems: [0] } - ] - } - activity = { - '@context': ['https://www.w3.org/ns/activitystreams'], - type: 'Create', - id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3', - to: ['https://localhost/u/test'], - audience: ['as:Public'], - actor: ['https://localhost/u/test'], - object: [question], - shares: [{ - id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares', - type: 'OrderedCollection', - totalItems: [0], - first: ['https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares?page=true'] - }], - likes: [{ - id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes', - type: 'OrderedCollection', - totalItems: [0], - first: ['https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes?page=true'] - }] - } - reply = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://localhost/s/2131231", - "to": "https://localhost/u/test", - "actor": "https://localhost/u/test", - "type": "Create", - "object": { - "id": "https://localhost/o/2131231", - "type": "Note", - "name": "Yes", - "attributedTo": "https://localhost/u/test", - "to": "https://localhost/u/test", - "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" } + ] + } + activity = { + '@context': ['https://www.w3.org/ns/activitystreams'], + type: 'Create', + id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3', + to: ['https://localhost/u/test'], + audience: ['as:Public'], + actor: ['https://localhost/u/test'], + object: [question], + shares: [{ + id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares', + type: 'OrderedCollection', + totalItems: [0], + first: ['https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/shares?page=true'] + }], + likes: [{ + id: 'https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes', + type: 'OrderedCollection', + totalItems: [0], + first: ['https://localhost/s/a29a6843-9feb-4c74-a7f7-081b9c9201d3/likes?page=true'] + }] + } + reply = { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://localhost/s/2131231', + to: 'https://localhost/u/test', + actor: 'https://localhost/u/test', + type: 'Create', + object: { + id: 'https://localhost/o/2131231', + type: 'Note', + name: 'Yes', + attributedTo: 'https://localhost/u/test', + to: 'https://localhost/u/test', + inReplyTo: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19' } - await apex.store.saveActivity(activity) - await apex.store.saveObject(question) - }) - it('tracks replies in a collection', async function () { - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(200) + } + await apex.store.saveActivity(activity) + await apex.store.saveObject(question) + }) + it('tracks replies in a collection', async function () { + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) - storedReply = await apex.store.getActivity(reply.id, true) - expect(storedReply._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes') + storedReply = await apex.store.getActivity(reply.id, true) + expect(storedReply._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes') + }) + it('the question replies collection is updated', async function () { + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + + const questionStored = await apex.store.getObject(question.id, true) + const chosenCollection = questionStored.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') + expect(chosenCollection.replies.totalItems[0]).toBe(1) + }) + it('keeps a voterCount tally', async function () { + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + let questionStored = await apex.store.getObject(question.id, true) + expect(questionStored.votersCount).toEqual(1) + const anotherVoter = await apex.createActor('voter', 'voter', 'voting user') + await apex.store.saveObject(anotherVoter) + const anotherReply = { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://localhost/s/2131232', + to: 'https://localhost/u/test', + actor: 'https://localhost/u/voter', + type: 'Create', + object: { + id: 'https://localhost/o/2131232', + type: 'Note', + name: 'Yes', + attributedTo: 'https://localhost/u/voter', + to: 'https://localhost/u/test', + inReplyTo: 'https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19' + } + } + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(anotherReply) + .expect(200) + questionStored = await apex.store.getObject(question.id, true) + expect(questionStored.votersCount).toEqual(2) + }) + it('anyOf property allows a user to vote for multiple choices', async function () { + question.anyOf = question.oneOf + delete question.oneOf + await apex.store.updateObject(question, 'test', true) + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + reply.id = 'https://localhost/s/2131232' + reply.object.id = 'https://localhost/o/2131232' + reply.object.name = 'No' + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') + .send(reply) + .expect(200) + let questionStored = await apex.store.getObject(question.id, true) + const yesCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') + expect(yesCollection.replies.totalItems[0]).toBe(1) + const noCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'no') + expect(noCollection.replies.totalItems[0]).toBe(1) + questionStored = await apex.store.getObject(question.id, true) + expect(questionStored.votersCount).toEqual(1) + }) + it('publishes the results', async function () { + const addrSpy = spyOn(apex, 'address').and.callFake(async () => ['https://ignore.com/inbox/ignored']) + const requestValidated = new Promise(resolve => { + nock('https://mocked.com').post('/inbox/mocked') + .reply(200) + .on('request', async (req, interceptor, body) => { + const sentActivity = JSON.parse(body) + expect(sentActivity.object.votersCount).toEqual(1) + expect(sentActivity.object.oneOf.find(({ name }) => name.toLowerCase() === 'yes').replies.totalItems).toEqual(1) + resolve() + }) }) - it('the question replies collection is updated', async function () { + addrSpy.and.callFake(async () => ['https://mocked.com/inbox/mocked']) + await request(app) + .post('/inbox/test') + .set('Content-Type', 'application/activity+json') + .send(reply) + .expect(200) + await requestValidated + }) + describe('validations', function () { + it('wont allow a vote to a closed poll', async function () { + const closedDate = new Date() + closedDate.setDate(closedDate.getDate() - 1) + question.endTime = closedDate + await apex.store.updateObject(question, 'test', true) await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(reply) - .expect(200) - - let questionStored = await apex.store.getObject(question.id, true) - let chosenCollection = questionStored.oneOf.find(({ name }) => name[0].toLowerCase() === 'yes') - expect(chosenCollection.replies.totalItems[0]).toBe(1) + .expect(403) }) - it('keeps a voterCount tally', async function () { + it('prevents the same user from voting for the same choice twice', async function () { + question.anyOf = question.oneOf + delete question.oneOf + await apex.store.updateObject(question, 'test', true) await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(reply) .expect(200) - let questionStored = await apex.store.getObject(question.id, true) - expect(questionStored.votersCount).toEqual(1) - let anotherVoter = await apex.createActor('voter', 'voter', 'voting user') - await apex.store.saveObject(anotherVoter) - let anotherReply = { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://localhost/s/2131232", - "to": "https://localhost/u/test", - "actor": "https://localhost/u/voter", - "type": "Create", - "object": { - "id": "https://localhost/o/2131232", - "type": "Note", - "name": "Yes", - "attributedTo": "https://localhost/u/voter", - "to": "https://localhost/u/test", - "inReplyTo": "https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19" - } - } + reply.id = 'https://localhost/s/2131232' + reply.object.id = 'https://localhost/o/2131232' await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(anotherReply) - .expect(200) - questionStored = await apex.store.getObject(question.id, true) - expect(questionStored.votersCount).toEqual(2) + .send(reply) + .expect(403) }) - it('anyOf property allows a user to vote for multiple choices', async function () { - question.anyOf = question.oneOf - delete question.oneOf - await apex.store.updateObject(question, 'test', true) + it('oneOf prevents the same user from voting for multiple choices', async function () { await request(app) .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') @@ -1654,79 +1725,8 @@ describe('inbox', function () { .post('/inbox/test') .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') .send(reply) - .expect(200) - let questionStored = await apex.store.getObject(question.id, true) - let yesCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'yes') - expect(yesCollection.replies.totalItems[0]).toBe(1) - let noCollection = questionStored.anyOf.find(({ name }) => name[0].toLowerCase() === 'no') - expect(noCollection.replies.totalItems[0]).toBe(1) - questionStored = await apex.store.getObject(question.id, true) - expect(questionStored.votersCount).toEqual(1) - }) - it('publishes the results', async function () { - let addrSpy = spyOn(apex, 'address').and.callFake(async () => ['https://ignore.com/inbox/ignored']) - const requestValidated = new Promise(resolve => { - nock('https://mocked.com').post('/inbox/mocked') - .reply(200) - .on('request', async (req, interceptor, body) => { - const sentActivity = JSON.parse(body) - expect(sentActivity.object.votersCount).toEqual(1) - expect(sentActivity.object.oneOf.find(({ name }) => name.toLowerCase() === 'yes').replies.totalItems).toEqual(1) - resolve() - }) - }) - addrSpy.and.callFake(async () => ['https://mocked.com/inbox/mocked']) - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/activity+json') - .send(reply) - .expect(200) - await requestValidated - }) - describe('validations', function() { - it('wont allow a vote to a closed poll', async function () { - let closedDate = new Date() - closedDate.setDate(closedDate.getDate() - 1) - question.endTime = closedDate - await apex.store.updateObject(question, 'test', true) - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(403) - }) - it('prevents the same user from voting for the same choice twice', async function () { - question.anyOf = question.oneOf - delete question.oneOf - await apex.store.updateObject(question, 'test', true) - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(200) - reply.id = 'https://localhost/s/2131232' - reply.object.id = 'https://localhost/o/2131232' - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(403) - }) - it('oneOf prevents the same user from voting for multiple choices', async function () { - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(200) - reply.id = 'https://localhost/s/2131232' - reply.object.id = 'https://localhost/o/2131232' - reply.object.name = 'No' - await request(app) - .post('/inbox/test') - .set('Content-Type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"') - .send(reply) - .expect(403) - }) + .expect(403) }) + }) }) }) From 9f1e1fbb008c70a43c6c8b429d267ffd4ac5cc9c Mon Sep 17 00:00:00 2001 From: aspalding Date: Mon, 30 Oct 2023 11:52:03 -0500 Subject: [PATCH 27/28] manual lint fixes --- net/activity.js | 3 ++- net/validators.js | 2 +- spec/functional/inbox.spec.js | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/net/activity.js b/net/activity.js index 479a060..ae6c77b 100644 --- a/net/activity.js +++ b/net/activity.js @@ -81,6 +81,7 @@ module.exports = { let activity = req.body let object = resLocal.object resLocal.status = 200 + let question /* isNewActivity: false - this inbox has seen this activity before 'new collection' - known activity, new inbox @@ -190,7 +191,7 @@ module.exports = { } break case 'create': - const question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') + question = resLocal.linked.find(({ type }) => type.toLowerCase() === 'question') if (question) { let questionType const targetActivity = object diff --git a/net/validators.js b/net/validators.js index c0fbbe2..55e93b6 100644 --- a/net/validators.js +++ b/net/validators.js @@ -151,7 +151,7 @@ function inboxActivity (req, res, next) { } } else if (Object.hasOwn(question, 'anyOf')) { const hasDuplicateVote = question._meta?.voteAndVoter[0].some(({ voter, voteName }) => { - return voter === activity.actor[0] && activity.object[0].name == voteName + return voter === activity.actor[0] && activity.object[0].name === voteName }) if (hasDuplicateVote) { resLocal.status = 403 diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index 6c01ef9..cf71bd8 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1511,7 +1511,7 @@ describe('inbox', function () { }) }) - fdescribe('question', function () { + describe('question', function () { let activity let question let reply @@ -1591,7 +1591,7 @@ describe('inbox', function () { .send(reply) .expect(200) - storedReply = await apex.store.getActivity(reply.id, true) + const storedReply = await apex.store.getActivity(reply.id, true) expect(storedReply._meta.collection).toContain('https://localhost/o/49e2d03d-b53a-4c4c-a95c-94a6abf45a19/votes/Yes') }) it('the question replies collection is updated', async function () { From b6b568f0a2ae984b7a587bd8dda9e622725efa6c Mon Sep 17 00:00:00 2001 From: aspalding Date: Mon, 30 Oct 2023 12:20:43 -0500 Subject: [PATCH 28/28] correctly resolve lint issue --- net/validators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/net/validators.js b/net/validators.js index 55e93b6..4587f0f 100644 --- a/net/validators.js +++ b/net/validators.js @@ -151,7 +151,7 @@ function inboxActivity (req, res, next) { } } else if (Object.hasOwn(question, 'anyOf')) { const hasDuplicateVote = question._meta?.voteAndVoter[0].some(({ voter, voteName }) => { - return voter === activity.actor[0] && activity.object[0].name === voteName + return voter === activity.actor[0] && activity.object[0].name[0] === voteName }) if (hasDuplicateVote) { resLocal.status = 403