-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Notify comment author about change of subscription to post comments
The 'post:update' realtime event can now be fired to a comment author when comment is created or deleted. It happens when the action changes the user's subscription to comments state (the 'notifyOfAllComments' field of the serialized post).
- Loading branch information
Showing
7 changed files
with
219 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { Middleware } from 'koa'; | ||
import { isEqual } from 'lodash'; | ||
|
||
import { type User, type Post, PubSub as pubSub, dbAdapter } from '../../models'; | ||
import { ServerErrorException } from '../../support/exceptions'; | ||
import { List } from '../../support/open-lists'; | ||
|
||
type State = { post?: Post; user?: User }; | ||
|
||
/** | ||
* Middleware that checks if user specific properties of the post was changed | ||
* during the request and emits an RT message if so. | ||
*/ | ||
export function postMayUpdateForUser( | ||
selectUser = (s: State) => Promise.resolve(s.user), | ||
): Middleware<State> { | ||
return async (ctx, next) => { | ||
let { post } = ctx.state; | ||
const user = await selectUser(ctx.state); | ||
|
||
if (!post) { | ||
throw new ServerErrorException( | ||
`Server misconfiguration: the required parameter 'postId' is missing`, | ||
); | ||
} | ||
|
||
if (!user) { | ||
return await next(); | ||
} | ||
|
||
const propsBefore = await post.getUserSpecificProps(user); | ||
|
||
const result = await next(); | ||
|
||
// Re-read updated post from DB | ||
post = (await dbAdapter.getPostById(post.id))!; | ||
const propsAfter = await post.getUserSpecificProps(user); | ||
|
||
if (!isEqual(propsBefore, propsAfter)) { | ||
// Emit RT message | ||
await pubSub.updatePost(post.id, { onlyForUsers: List.from([user.id]) }); | ||
} | ||
|
||
return result; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
/* eslint-env node, mocha */ | ||
import expect from 'unexpected'; | ||
|
||
import { getSingleton } from '../../app/app'; | ||
import { PubSub } from '../../app/models'; | ||
import { PubSubAdapter, eventNames as ev } from '../../app/support/PubSubAdapter'; | ||
import redisDb from '../../app/setup/database'; | ||
import { connect as pgConnect } from '../../app/setup/postgres'; | ||
import cleanDB from '../dbCleaner'; | ||
|
||
import Session from './realtime-session'; | ||
import { | ||
createTestUsers, | ||
createAndReturnPost, | ||
updateUserAsync, | ||
createCommentAsync, | ||
performJSONRequest, | ||
authHeaders, | ||
} from './functional_test_helper'; | ||
|
||
describe('Realtime of post comment events', () => { | ||
let port; | ||
|
||
before(async () => { | ||
const app = await getSingleton(); | ||
port = process.env.PEPYATKA_SERVER_PORT || app.context.config.port; | ||
const pubsubAdapter = new PubSubAdapter(redisDb); | ||
PubSub.setPublisher(pubsubAdapter); | ||
}); | ||
|
||
let luna, mars, lunaSession, marsSession; | ||
|
||
beforeEach(async () => { | ||
await cleanDB(pgConnect()); | ||
|
||
[luna, mars] = await createTestUsers(['luna', 'mars']); | ||
|
||
[lunaSession, marsSession] = await Promise.all([ | ||
Session.create(port, 'Luna session'), | ||
Session.create(port, 'Mars session'), | ||
]); | ||
|
||
await Promise.all([ | ||
lunaSession.sendAsync('auth', { authToken: luna.authToken }), | ||
marsSession.sendAsync('auth', { authToken: mars.authToken }), | ||
]); | ||
}); | ||
|
||
describe(`Mars wants to receive comments on commented post`, () => { | ||
beforeEach(async () => { | ||
await updateUserAsync(mars, { preferences: { notifyOfCommentsOnCommentedPosts: true } }); | ||
}); | ||
|
||
describe(`Luna creates post, Luna & Mars subscribes to it`, () => { | ||
let post; | ||
beforeEach(async () => { | ||
post = await createAndReturnPost(luna, 'Luna post'); | ||
await Promise.all([ | ||
lunaSession.sendAsync('subscribe', { post: [post.id] }), | ||
marsSession.sendAsync('subscribe', { post: [post.id] }), | ||
]); | ||
}); | ||
|
||
it(`should not deliver ${ev.POST_UPDATED} event to Luna after Luna's comment`, async () => { | ||
const test = lunaSession.notReceiveWhile(ev.POST_UPDATED, () => | ||
createCommentAsync(luna, post.id, 'Hello'), | ||
); | ||
await expect(test, 'to be fulfilled'); | ||
}); | ||
|
||
it(`should not deliver ${ev.POST_UPDATED} event to Mars after Luna's comment`, async () => { | ||
const test = marsSession.notReceiveWhile(ev.POST_UPDATED, () => | ||
createCommentAsync(luna, post.id, 'Hello'), | ||
); | ||
await expect(test, 'to be fulfilled'); | ||
}); | ||
|
||
it(`should not deliver ${ev.POST_UPDATED} event to Luna after Mars' comment`, async () => { | ||
const test = lunaSession.notReceiveWhile(ev.POST_UPDATED, () => | ||
createCommentAsync(mars, post.id, 'Hello'), | ||
); | ||
await expect(test, 'to be fulfilled'); | ||
}); | ||
|
||
it(`should deliver ${ev.POST_UPDATED} event with 'notifyOfAllComments: true' to Mars after Mars' comment`, async () => { | ||
const test = marsSession.receiveWhile(ev.POST_UPDATED, () => | ||
createCommentAsync(mars, post.id, 'Hello'), | ||
); | ||
await expect(test, 'to be fulfilled with', { posts: { notifyOfAllComments: true } }); | ||
}); | ||
|
||
it(`should deliver ${ev.POST_UPDATED} event with 'notifyOfAllComments: false' to Mars after Mars removes their comment`, async () => { | ||
let commentId; | ||
|
||
{ | ||
const test = marsSession.receiveWhile(ev.POST_UPDATED, async () => { | ||
const resp = await createCommentAsync(mars, post.id, 'Hello').then((r) => r.json()); | ||
commentId = resp.comments.id; | ||
}); | ||
await expect(test, 'to be fulfilled with', { posts: { notifyOfAllComments: true } }); | ||
} | ||
|
||
{ | ||
const test = marsSession.receiveWhile(ev.POST_UPDATED, () => | ||
performJSONRequest('DELETE', `/v2/comments/${commentId}`, null, authHeaders(mars)), | ||
); | ||
await expect(test, 'to be fulfilled with', { posts: { notifyOfAllComments: false } }); | ||
} | ||
}); | ||
|
||
it(`should deliver ${ev.POST_UPDATED} event with 'notifyOfAllComments: false' to Mars after Luna removes Mars' comment`, async () => { | ||
let commentId; | ||
|
||
{ | ||
const test = marsSession.receiveWhile(ev.POST_UPDATED, async () => { | ||
const resp = await createCommentAsync(mars, post.id, 'Hello').then((r) => r.json()); | ||
commentId = resp.comments.id; | ||
}); | ||
await expect(test, 'to be fulfilled with', { posts: { notifyOfAllComments: true } }); | ||
} | ||
|
||
{ | ||
const test = marsSession.receiveWhile(ev.POST_UPDATED, () => | ||
performJSONRequest('DELETE', `/v2/comments/${commentId}`, null, authHeaders(luna)), | ||
); | ||
await expect(test, 'to be fulfilled with', { posts: { notifyOfAllComments: false } }); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); |