feat: Add `push.test` endpoint (#31289)

pull/31328/head^2
Matheus Barbosa Silva 2 years ago committed by GitHub
parent 6587e0d4be
commit 7c6198f49f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/kind-melons-try.md
  2. 25
      apps/meteor/app/api/server/v1/push.ts
  3. 4
      apps/meteor/app/push/server/push.ts
  4. 62
      apps/meteor/server/lib/pushConfig.ts
  5. 27
      apps/meteor/server/models/raw/AppsTokens.ts
  6. 162
      apps/meteor/tests/end-to-end/api/22-push.ts
  7. 8
      packages/model-typings/src/models/IAppsTokensModel.ts
  8. 5
      packages/rest-typings/src/v1/push.ts

@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/rest-typings": patch
---
Added `push.test` POST endpoint for sending test push notification to user (requires `test-push-notifications` permission)

@ -3,6 +3,7 @@ import { Random } from '@rocket.chat/random';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { executePushTest } from '../../../../server/lib/pushConfig';
import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom';
import PushNotification from '../../../push-notifications/server/lib/PushNotification';
import { settings } from '../../../settings/server';
@ -126,3 +127,27 @@ API.v1.addRoute(
},
},
);
API.v1.addRoute(
'push.test',
{
authRequired: true,
rateLimiterOptions: {
numRequestsAllowed: 1,
intervalTimeInMS: 1000,
},
permissionsRequired: ['test-push-notifications'],
},
{
async post() {
if (settings.get('Push_enable') !== true) {
throw new Meteor.Error('error-push-disabled', 'Push is disabled', {
method: 'push_test',
});
}
const tokensCount = await executePushTest(this.userId, this.user.username);
return API.v1.success({ tokensCount });
},
},
);

@ -283,11 +283,11 @@ class PushClass {
logger.debug('GUIDE: The "AppsTokens" is empty - No clients have registered on the server yet...');
}
} else if (!countApn.length) {
if ((await AppsTokens.col.countDocuments({ 'token.apn': { $exists: true } })) === 0) {
if ((await AppsTokens.countApnTokens()) === 0) {
logger.debug('GUIDE: The "AppsTokens" - No APN clients have registered on the server yet...');
}
} else if (!countGcm.length) {
if ((await AppsTokens.col.countDocuments({ 'token.gcm': { $exists: true } })) === 0) {
if ((await AppsTokens.countGcmTokens()) === 0) {
logger.debug('GUIDE: The "AppsTokens" - No GCM clients have registered on the server yet...');
}
}

@ -1,3 +1,4 @@
import type { IUser } from '@rocket.chat/core-typings';
import { AppsTokens } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
@ -9,6 +10,26 @@ import { Push } from '../../app/push/server';
import { settings } from '../../app/settings/server';
import { i18n } from './i18n';
export const executePushTest = async (userId: IUser['_id'], username: IUser['username']): Promise<number> => {
const tokens = await AppsTokens.countTokensByUserId(userId);
if (tokens === 0) {
throw new Meteor.Error('error-no-tokens-for-this-user', 'There are no tokens for this user', {
method: 'push_test',
});
}
await Push.send({
from: 'push',
title: `@${username}`,
text: i18n.t('This_is_a_push_test_messsage'),
sound: 'default',
userId,
});
return tokens;
};
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface ServerMethods {
@ -38,47 +59,10 @@ Meteor.methods<ServerMethods>({
});
}
const query = {
$and: [
{
userId: user._id,
},
{
$or: [
{
'token.apn': {
$exists: true,
},
},
{
'token.gcm': {
$exists: true,
},
},
],
},
],
};
const tokens = await AppsTokens.col.countDocuments(query);
if (tokens === 0) {
throw new Meteor.Error('error-no-tokens-for-this-user', 'There are no tokens for this user', {
method: 'push_test',
});
}
await Push.send({
from: 'push',
title: `@${user.username}`,
text: i18n.t('This_is_a_push_test_messsage'),
sound: 'default',
userId: user._id,
});
const tokensCount = await executePushTest(user._id, user.username);
return {
message: 'Your_push_was_sent_to_s_devices',
params: [tokens],
params: [tokensCount],
};
},
});

@ -1,4 +1,4 @@
import type { IAppsTokens } from '@rocket.chat/core-typings';
import type { IAppsTokens, IUser } from '@rocket.chat/core-typings';
import type { IAppsTokensModel } from '@rocket.chat/model-typings';
import type { Db } from 'mongodb';
@ -8,4 +8,29 @@ export class AppsTokens extends BaseRaw<IAppsTokens> implements IAppsTokensModel
constructor(db: Db) {
super(db, '_raix_push_app_tokens', undefined, { collectionNameResolver: (name) => name });
}
countApnTokens() {
const query = {
'token.apn': { $exists: true },
};
return this.countDocuments(query);
}
countGcmTokens() {
const query = {
'token.gcm': { $exists: true },
};
return this.countDocuments(query);
}
countTokensByUserId(userId: IUser['_id']) {
const query = {
userId,
$or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }],
};
return this.countDocuments(query);
}
}

@ -2,26 +2,26 @@ import { expect } from 'chai';
import { before, describe, it } from 'mocha';
import { getCredentials, api, request, credentials } from '../../data/api-data.js';
import { updateSetting } from '../../data/permissions.helper';
describe('push token', function () {
describe('[Push]', function () {
this.retries(0);
before((done) => getCredentials(done));
describe('POST [/push.token]', () => {
it('should fail if not logged in', (done) => {
request
it('should fail if not logged in', async () => {
await request
.post(api('push.token'))
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
})
.end(done);
});
});
it('should fail if missing type', (done) => {
request
it('should fail if missing type', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
@ -32,12 +32,11 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-type-param-not-valid');
})
.end(done);
});
});
it('should fail if missing value', (done) => {
request
it('should fail if missing value', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
@ -48,12 +47,11 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-token-param-not-valid');
})
.end(done);
});
});
it('should fail if missing appName', (done) => {
request
it('should fail if missing appName', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
@ -64,12 +62,11 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-appName-param-not-valid');
})
.end(done);
});
});
it('should fail if type param is unknown', (done) => {
request
it('should fail if type param is unknown', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
@ -79,12 +76,11 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-type-param-not-valid');
})
.end(done);
});
});
it('should fail if token param is empty', (done) => {
request
it('should fail if token param is empty', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
@ -96,12 +92,11 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-token-param-not-valid');
})
.end(done);
});
});
it('should add a token if valid', (done) => {
request
it('should add a token if valid', async () => {
await request
.post(api('push.token'))
.set(credentials)
.send({
@ -113,14 +108,13 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('result').and.to.be.an('object');
})
.end(done);
});
});
});
describe('DELETE [/push.token]', () => {
it('should fail if not logged in', (done) => {
request
it('should fail if not logged in', async () => {
await request
.delete(api('push.token'))
.send({
token: 'token',
@ -129,12 +123,11 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
})
.end(done);
});
});
it('should fail if missing token key', (done) => {
request
it('should fail if missing token key', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({})
@ -142,12 +135,11 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-token-param-not-valid');
})
.end(done);
});
});
it('should fail if token is empty', (done) => {
request
it('should fail if token is empty', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({
@ -157,12 +149,11 @@ describe('push token', function () {
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-token-param-not-valid');
})
.end(done);
});
});
it('should fail if token is invalid', (done) => {
request
it('should fail if token is invalid', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({
@ -171,12 +162,11 @@ describe('push token', function () {
.expect(404)
.expect((res) => {
expect(res.body).to.have.property('success', false);
})
.end(done);
});
});
it('should delete a token if valid', (done) => {
request
it('should delete a token if valid', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({
@ -185,12 +175,11 @@ describe('push token', function () {
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
})
.end(done);
});
});
it('should fail if token is already deleted', (done) => {
request
it('should fail if token is already deleted', async () => {
await request
.delete(api('push.token'))
.set(credentials)
.send({
@ -199,8 +188,75 @@ describe('push token', function () {
.expect(404)
.expect((res) => {
expect(res.body).to.have.property('success', false);
})
.end(done);
});
});
});
describe('[/push.info]', () => {
before(async () => {
await updateSetting('Push_enable', true);
await updateSetting('Push_enable_gateway', true);
await updateSetting('Push_gateway', 'https://random-gateway.rocket.chat');
});
it('should fail if not logged in', async () => {
await request
.get(api('push.info'))
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should succesfully retrieve non default push notification info', async () => {
await request
.get(api('push.info'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('pushGatewayEnabled', true);
expect(res.body).to.have.property('defaultPushGateway', false);
});
});
it('should succesfully retrieve default push notification info', async () => {
await updateSetting('Push_gateway', 'https://gateway.rocket.chat');
await request
.get(api('push.info'))
.set(credentials)
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('pushGatewayEnabled', true);
expect(res.body).to.have.property('defaultPushGateway', true);
});
});
});
describe('[/push.test]', () => {
before(async () => {
await updateSetting('Push_enable', false);
});
it('should fail if not logged in', async () => {
await request
.post(api('push.test'))
.expect(401)
.expect((res) => {
expect(res.body).to.have.property('status', 'error');
expect(res.body).to.have.property('message');
});
});
it('should fail if push is disabled', async () => {
await request
.post(api('push.test'))
.set(credentials)
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('errorType', 'error-push-disabled');
});
});
});
});

@ -1,5 +1,9 @@
import type { IAppsTokens } from '@rocket.chat/core-typings';
import type { IAppsTokens, IUser } from '@rocket.chat/core-typings';
import type { IBaseModel } from './IBaseModel';
export type IAppsTokensModel = IBaseModel<IAppsTokens>;
export interface IAppsTokensModel extends IBaseModel<IAppsTokens> {
countTokensByUserId(userId: IUser['_id']): Promise<number>;
countGcmTokens(): Promise<number>;
countApnTokens(): Promise<number>;
}

@ -71,4 +71,9 @@ export type PushEndpoints = {
defaultPushGateway: boolean;
};
};
'/v1/push.test': {
POST: () => {
tokensCount: boolean;
};
};
};

Loading…
Cancel
Save