diff --git a/apps/condo/domains/news/access/GetNewsSharingRecipientsService.js b/apps/condo/domains/news/access/GetNewsSharingRecipientsService.js new file mode 100644 index 00000000000..50e393826a2 --- /dev/null +++ b/apps/condo/domains/news/access/GetNewsSharingRecipientsService.js @@ -0,0 +1,31 @@ +/** + * Generated by `createservice news.GetNewsItemSharingRecipientsService --type queries` + */ +const get = require('lodash/get') + +const { throwAuthenticationError } = require('@open-condo/keystone/apolloErrorFormatter') +const { getById } = require('@open-condo/keystone/schema') + +const { checkPermissionInUserOrganizationOrRelatedOrganization } = require('@condo/domains/organization/utils/accessSchema') + +async function canGetNewsSharingRecipients ({ authentication: { item: user }, args: { data: { b2bAppContext: { id: b2bAppContextId } } } }) { + if (!user) return throwAuthenticationError() + if (user.deletedAt) return false + if (user.isAdmin || user.isSupport) return true + + const b2bContext = await getById('B2BAppContext', b2bAppContextId) + if (!b2bContext) { return false } + + const organizationId = get(b2bContext, 'organization', null) + if (!organizationId) { return false } + + return await checkPermissionInUserOrganizationOrRelatedOrganization(user.id, organizationId, 'canReadNewsItems') +} + +/* + Rules are logical functions that used for list access, and may return a boolean (meaning + all or no items are available) or a set of filters that limit the available items. +*/ +module.exports = { + canGetNewsSharingRecipients, +} \ No newline at end of file diff --git a/apps/condo/domains/news/gql.js b/apps/condo/domains/news/gql.js index dd8f242f240..6b8262725c0 100644 --- a/apps/condo/domains/news/gql.js +++ b/apps/condo/domains/news/gql.js @@ -36,6 +36,12 @@ const NewsItemRecipientsExportTask = generateGqlQueries('NewsItemRecipientsExpor const NEWS_ITEM_SHARING_FIELDS = `{ b2bAppContext { id } newsItem { id } sharingParams status statusMessage lastPostRequest ${COMMON_FIELDS} }` const NewsItemSharing = generateGqlQueries('NewsItemSharing', NEWS_ITEM_SHARING_FIELDS) +const GET_NEWS_SHARING_RECIPIENTS_MUTATION = gql` + query getGetNewsSharingRecipients ($data: GetNewsSharingRecipientsInput!) { + result: getNewsSharingRecipients(data: $data) { id name recipients } + } +` + /* AUTOGENERATE MARKER */ module.exports = { @@ -46,5 +52,6 @@ module.exports = { GET_NEWS_ITEMS_RECIPIENTS_COUNTERS_MUTATION, NewsItemRecipientsExportTask, NewsItemSharing, + GET_NEWS_SHARING_RECIPIENTS_MUTATION, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/news/schema/GetNewsSharingRecipientsService.js b/apps/condo/domains/news/schema/GetNewsSharingRecipientsService.js new file mode 100644 index 00000000000..73a7d173ec4 --- /dev/null +++ b/apps/condo/domains/news/schema/GetNewsSharingRecipientsService.js @@ -0,0 +1,159 @@ +/** + * Generated by `createservice news.GetNewsItemSharingRecipientsService --type queries` + */ + +/** + * This service is part of "NewsSharing" functionality. + * + * -- + * + * To make your miniapp able to show up on /news page you need to implement getRecipients method. + * + * GetRecipients should return a list of available recipients with relevant data: + * + * Request: + * + * GET ?organizationId=string + * + * Where: + * - URL = getRecipeints endpoint url provided in NewsItemSharingConfig + * + * Response payload: + * [{ + * id: + * subscribers: + * name: + * }, + * {...} + * ] + * + * GetRecipients endpoint should be covered behind BASIC auth + * + * -- + * + * This method is just a proxy between Condo and miniapp. + ** + * -> -> BASIC-AUTH -> + */ + +const fetch = require('node-fetch') + +const { GQLError, GQLErrorCode: { BAD_USER_INPUT, INTERNAL_ERROR } } = require('@open-condo/keystone/errors') +const { GQLCustomSchema, getById } = require('@open-condo/keystone/schema') + +const { WRONG_VALUE, NETWORK_ERROR } = require('@condo/domains/common/constants/errors') +const access = require('@condo/domains/news/access/GetNewsSharingRecipientsService') + +/** + * List of possible errors, that this custom schema can throw + * They will be rendered in documentation section in GraphiQL for this custom schema + */ +const ERRORS = { + NOT_NEWS_SHARING_APP: { + query: 'getNewsItemSharingRecipients', + variable: ['data', 'b2bAppContext'], + code: BAD_USER_INPUT, + type: WRONG_VALUE, + message: 'Provided b2bApp does not support NewsItemSharing', + messageForUser: 'api.newsItem.getNewsSharingRecipients.NOT_NEWS_SHARING_APP', + }, + NEWS_SHARING_APP_REQUEST_FAILED: { + query: 'getNewsItemSharingRecipients', + variable: ['data'], + code: INTERNAL_ERROR, + type: NETWORK_ERROR, + message: 'Could not get a successful response from NewsSharing miniapp', + messageForUser: 'api.newsItem.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_FAILED', + }, + NEWS_SHARING_APP_REQUEST_BAD_RESPONSE: { + query: 'getNewsItemSharingRecipients', + variable: ['data'], + code: INTERNAL_ERROR, + type: WRONG_VALUE, + message: 'Response from NewsSharing miniapp was successful, but the data format was incorrect', + messageForUser: 'api.newsItem.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE', + }, +} + +const GetNewsSharingRecipientsService = new GQLCustomSchema('GetNewsSharingRecipientsService', { + types: [ + { + access: true, + type: 'input GetNewsSharingRecipientsInput { dv: Int!, sender: JSON!, b2bAppContext: B2BAppContextWhereUniqueInput! }', + }, + { + access: true, + type: 'type GetNewsSharingRecipientsOutput { id: String!, name: String!, recipients: Int }', + }, + ], + + queries: [ + { + access: access.canGetNewsSharingRecipients, + schema: 'getNewsSharingRecipients (data: GetNewsSharingRecipientsInput!): [GetNewsSharingRecipientsOutput]', + resolver: async (parent, args, context, info, extra = {}) => { + const { data } = args + + const { dv, sender, b2bAppContext } = data + + const b2bAppContextData = await getById('B2BAppContext', b2bAppContext.id) + const b2bApp = await getById('B2BApp', b2bAppContextData.app) + + if (!b2bApp.newsSharingConfig) { + throw new GQLError(ERRORS.NOT_NEWS_SHARING_APP) + } + + const newsSharingConfig = await getById('B2BAppNewsSharingConfig', b2bApp.newsSharingConfig) + + if (!newsSharingConfig) { + throw new GQLError(ERRORS.NOT_NEWS_SHARING_APP) + } + + const getRecipientsUrl = newsSharingConfig.getRecipientsUrl + const organizationId = b2bAppContextData.organization + + let getRecipientsResult + + // Check that we can obtain result data + try { + getRecipientsResult = await fetch( + `${getRecipientsUrl}?organizationId=${organizationId}`, + ) + } + catch (err) { + throw new GQLError(ERRORS.NEWS_SHARING_APP_REQUEST_FAILED) + } + + // If status code of response is not 200, we need to raise an error + if (getRecipientsResult.status !== 200) { + throw new GQLError(ERRORS.NEWS_SHARING_APP_REQUEST_FAILED) + } + + // Check that result data is in good shape + const getRecipientsResultData = await getRecipientsResult.json() + + if (!getRecipientsResultData || !Array.isArray(getRecipientsResultData)) { + throw new GQLError(ERRORS.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE) + } + + getRecipientsResultData.forEach(x => { + if ( + (typeof x.id !== 'string') || + (typeof x.name !== 'string') || + (x.recipients && typeof x.recipients !== 'number') + ) { + throw new GQLError(ERRORS.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE) + } + }) + + return getRecipientsResultData + }, + }, + ], + +}) + +module.exports = { + GetNewsSharingRecipientsService, + ERRORS, +} diff --git a/apps/condo/domains/news/schema/GetNewsSharingRecipientsService.test.js b/apps/condo/domains/news/schema/GetNewsSharingRecipientsService.test.js new file mode 100644 index 00000000000..8be4720dc41 --- /dev/null +++ b/apps/condo/domains/news/schema/GetNewsSharingRecipientsService.test.js @@ -0,0 +1,170 @@ +/** + * Generated by `createservice news.GetNewsItemSharingRecipientsService --type queries` + */ + +const conf = require('@open-condo/config') +const { makeLoggedInAdminClient, makeClient, expectToThrowAuthenticationError, expectToThrowAccessDeniedErrorToResult, expectToThrowGQLError } = require('@open-condo/keystone/test.utils') +const { expectToThrowAccessDeniedErrorToObj, expectToThrowAuthenticationErrorToObjects } = require('@open-condo/keystone/test.utils') + +const { + createTestB2BAppNewsSharingConfig, + createTestB2BApp, + createTestB2BAppContext, +} = require('@condo/domains/miniapp/utils/testSchema') +const { ERRORS} = require('@condo/domains/news/schema/GetNewsSharingRecipientsService') +const { getNewsSharingRecipientsByTestClient } = require('@condo/domains/news/utils/testSchema') +const { createTestOrganization } = require('@condo/domains/organization/utils/testSchema') + + +const { + createTestOrganizationEmployeeRole, + createTestOrganizationEmployee, +} = require('../../organization/utils/testSchema') +const { makeClientWithNewRegisteredAndLoggedInUser } = require('../../user/utils/testSchema') +const { + SUCCESS_GET_RECIPIENTS_URL, + FAULTY_GET_RECIPIENTS_URL_404, + FAULTY_GET_RECIPIENTS_URL_500, + INCORRECT_GET_RECIPIENTS_URL, + SUCCESS_PREVIEW_URL, + SUCCESS_PUBLISH_URL, + SUCCESS_GET_RECIPIENTS_RESULT, +} = require('../utils/testSchema/NewsSharingTestingApp') + +let adminClient, dummyO10n, staffClientWithPermissions, dummyB2BContextWithNewsSharingConfig + +describe('GetNewsSharingRecipientsService', () => { + beforeAll(async () => { + adminClient = await makeLoggedInAdminClient() + + const [o10n] = await createTestOrganization(adminClient) + dummyO10n = o10n + + staffClientWithPermissions = await makeClientWithNewRegisteredAndLoggedInUser() + const [roleYes] = await createTestOrganizationEmployeeRole(adminClient, o10n, { canReadNewsItems: true }) + await createTestOrganizationEmployee(adminClient, o10n, staffClientWithPermissions.user, roleYes) + + const [B2BAppNewsSharingConfig] = await createTestB2BAppNewsSharingConfig(adminClient, { + getRecipientsUrl: `${conf['SERVER_URL']}${SUCCESS_GET_RECIPIENTS_URL}`, + previewUrl: `${conf['SERVER_URL']}${SUCCESS_PREVIEW_URL}`, + publishUrl: `${conf['SERVER_URL']}${SUCCESS_PUBLISH_URL}`, + }) + const [B2BAppWithNewsSharing] = await createTestB2BApp(adminClient, { newsSharingConfig: { connect: { id: B2BAppNewsSharingConfig.id } } }) + const [B2BContextWithNewsSharingConfig] = await createTestB2BAppContext(adminClient, B2BAppWithNewsSharing, o10n) + dummyB2BContextWithNewsSharingConfig = B2BContextWithNewsSharingConfig + }) + + test('Admin can execute query', async () => { + const [data, attrs] = await getNewsSharingRecipientsByTestClient(adminClient, dummyB2BContextWithNewsSharingConfig) + + expect(data).toMatchObject(SUCCESS_GET_RECIPIENTS_RESULT) + }) + + test('staff with permission can execute', async () => { + const [data, attrs] = await getNewsSharingRecipientsByTestClient(staffClientWithPermissions, dummyB2BContextWithNewsSharingConfig) + + expect(data).toMatchObject(SUCCESS_GET_RECIPIENTS_RESULT) + }) + + test('fails if B2BContext doesnt have NewsSharingConfig', async () => { + const [B2BApp] = await createTestB2BApp(adminClient) + const [B2BContext] = await createTestB2BAppContext(adminClient, B2BApp, dummyO10n) + + await expectToThrowGQLError(async () => { + await getNewsSharingRecipientsByTestClient(staffClientWithPermissions, B2BContext) + }, { + type: ERRORS.NOT_NEWS_SHARING_APP.type, + code: ERRORS.NOT_NEWS_SHARING_APP.code, + message: ERRORS.NOT_NEWS_SHARING_APP.message, + }, 'result') + }) + + test('fails if remote server return bad response code: 404', async () => { + const [B2BAppFailingNewsSharingConfig] = await createTestB2BAppNewsSharingConfig(adminClient, { + getRecipientsUrl: `${conf['SERVER_URL']}${FAULTY_GET_RECIPIENTS_URL_404}`, + previewUrl: `${conf['SERVER_URL']}${SUCCESS_PREVIEW_URL}`, + publishUrl: `${conf['SERVER_URL']}${SUCCESS_PUBLISH_URL}`, + }) + const [B2BApp] = await createTestB2BApp(adminClient, { newsSharingConfig: { connect: { id: B2BAppFailingNewsSharingConfig.id } } }) + const [B2BContext] = await createTestB2BAppContext(adminClient, B2BApp, dummyO10n) + + await expectToThrowGQLError(async () => { + await getNewsSharingRecipientsByTestClient(staffClientWithPermissions, B2BContext) + }, { + type: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.type, + code: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.code, + message: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.message, + }, 'result') + }) + + test('fails if remote server return bad response code: 500', async () => { + const [B2BAppFailingNewsSharingConfig] = await createTestB2BAppNewsSharingConfig(adminClient, { + getRecipientsUrl: `${conf['SERVER_URL']}${FAULTY_GET_RECIPIENTS_URL_500}`, + previewUrl: `${conf['SERVER_URL']}${SUCCESS_PREVIEW_URL}`, + publishUrl: `${conf['SERVER_URL']}${SUCCESS_PUBLISH_URL}`, + }) + const [B2BApp] = await createTestB2BApp(adminClient, { newsSharingConfig: { connect: { id: B2BAppFailingNewsSharingConfig.id } } }) + const [B2BContext] = await createTestB2BAppContext(adminClient, B2BApp, dummyO10n) + + await expectToThrowGQLError(async () => { + await getNewsSharingRecipientsByTestClient(staffClientWithPermissions, B2BContext) + }, { + type: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.type, + code: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.code, + message: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.message, + }, 'result') + }) + + test('fails if remote server returns wrong data', async () => { + const [B2BAppFailingNewsSharingConfig] = await createTestB2BAppNewsSharingConfig(adminClient, { + getRecipientsUrl: `${conf['SERVER_URL']}${INCORRECT_GET_RECIPIENTS_URL}`, + previewUrl: `${conf['SERVER_URL']}${SUCCESS_PREVIEW_URL}`, + publishUrl: `${conf['SERVER_URL']}${SUCCESS_PUBLISH_URL}`, + }) + const [B2BApp] = await createTestB2BApp(adminClient, { newsSharingConfig: { connect: { id: B2BAppFailingNewsSharingConfig.id } } }) + const [B2BContext] = await createTestB2BAppContext(adminClient, B2BApp, dummyO10n) + + await expectToThrowGQLError(async () => { + await getNewsSharingRecipientsByTestClient(staffClientWithPermissions, B2BContext) + }, { + type: ERRORS.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE.type, + code: ERRORS.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE.code, + message: ERRORS.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE.message, + }, 'result') + }) + + test('fails if remote server is inaccessible', async () => { + const [B2BAppFailingNewsSharingConfig] = await createTestB2BAppNewsSharingConfig(adminClient, { + getRecipientsUrl: 'https://101.101.101.101:10101', + previewUrl: `${conf['SERVER_URL']}${SUCCESS_PREVIEW_URL}`, + publishUrl: `${conf['SERVER_URL']}${SUCCESS_PUBLISH_URL}`, + }) + const [B2BApp] = await createTestB2BApp(adminClient, { newsSharingConfig: { connect: { id: B2BAppFailingNewsSharingConfig.id } } }) + const [B2BContext] = await createTestB2BAppContext(adminClient, B2BApp, dummyO10n) + + await expectToThrowGQLError(async () => { + await getNewsSharingRecipientsByTestClient(staffClientWithPermissions, B2BContext) + }, { + type: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.type, + code: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.code, + message: ERRORS.NEWS_SHARING_APP_REQUEST_FAILED.message, + }, 'result') + }) + + test('anonymous can\'t execute', async () => { + const anonymousClient = await makeClient() + await expectToThrowAuthenticationError(async () => { + await getNewsSharingRecipientsByTestClient(anonymousClient, dummyB2BContextWithNewsSharingConfig) + }, 'result') + }) + + test('staff without permission can\'t execute', async () => { + const staffClientWithoutPermissions = await makeClientWithNewRegisteredAndLoggedInUser() + const [roleNo] = await createTestOrganizationEmployeeRole(adminClient, dummyO10n, { canReadNewsItems: false }) + await createTestOrganizationEmployee(adminClient, dummyO10n, staffClientWithoutPermissions.user, roleNo) + + await expectToThrowAccessDeniedErrorToResult(async () => { + await getNewsSharingRecipientsByTestClient(staffClientWithoutPermissions, dummyB2BContextWithNewsSharingConfig) + }) + }) +}) \ No newline at end of file diff --git a/apps/condo/domains/news/schema/index.js b/apps/condo/domains/news/schema/index.js index 77e33414fc4..920710ce763 100644 --- a/apps/condo/domains/news/schema/index.js +++ b/apps/condo/domains/news/schema/index.js @@ -4,6 +4,7 @@ */ const { GetNewsItemsRecipientsCountersService } = require('./GetNewsItemsRecipientsCountersService') +const { GetNewsSharingRecipientsService } = require('./GetNewsSharingRecipientsService') const { NewsItem } = require('./NewsItem') const { NewsItemRecipientsExportTask } = require('./NewsItemRecipientsExportTask') const { NewsItemScope } = require('./NewsItemScope') @@ -20,5 +21,6 @@ module.exports = { GetNewsItemsRecipientsCountersService, NewsItemRecipientsExportTask, NewsItemSharing, + GetNewsSharingRecipientsService, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/news/utils/serverSchema/index.js b/apps/condo/domains/news/utils/serverSchema/index.js index 44dd4a6c459..52af639e1da 100644 --- a/apps/condo/domains/news/utils/serverSchema/index.js +++ b/apps/condo/domains/news/utils/serverSchema/index.js @@ -14,6 +14,7 @@ const { EXPORT_NEWS_RECIPIENTS_MUTATION } = require('@condo/domains/news/gql') const { GET_NEWS_ITEMS_RECIPIENTS_COUNTERS_MUTATION } = require('@condo/domains/news/gql') const { NewsItemRecipientsExportTask: NewsItemRecipientsExportTaskGQL } = require('@condo/domains/news/gql') const { NewsItemSharing: NewsItemSharingGQL } = require('@condo/domains/news/gql') +const { GET_NEWS_SHARING_RECIPIENTS_MUTATION } = require('@condo/domains/news/gql') /* AUTOGENERATE MARKER */ const NewsItem = generateServerUtils(NewsItemGQL) @@ -48,6 +49,20 @@ async function getNewsItemsRecipientsCounters (context, data) { const NewsItemRecipientsExportTask = generateServerUtils(NewsItemRecipientsExportTaskGQL) const NewsItemSharing = generateServerUtils(NewsItemSharingGQL) +async function getNewsItemSharingRecipients (context, data) { + if (!context) throw new Error('no context') + if (!data) throw new Error('no data') + if (!data.sender) throw new Error('no data.sender') + // TODO(codegen): write getNewsItemSharingRecipients serverSchema guards + + return await execGqlWithoutAccess(context, { + query: GET_NEWS_SHARING_RECIPIENTS_MUTATION, + variables: { data: { dv: 1, ...data } }, + errorMessage: '[error] Unable to getNewsSharingRecipients', + dataPath: 'obj', + }) +} + /* AUTOGENERATE MARKER */ module.exports = { @@ -59,5 +74,6 @@ module.exports = { getNewsItemsRecipientsCounters, NewsItemRecipientsExportTask, NewsItemSharing, + getNewsItemSharingRecipients, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/domains/news/utils/testSchema/NewsSharingTestingApp.js b/apps/condo/domains/news/utils/testSchema/NewsSharingTestingApp.js new file mode 100644 index 00000000000..0c232c15dbf --- /dev/null +++ b/apps/condo/domains/news/utils/testSchema/NewsSharingTestingApp.js @@ -0,0 +1,88 @@ +const express = require('express') + +const SUCCESS_GET_RECIPIENTS_URL = "/test/news-sharing-api/success/getRecipients" +const SUCCESS_PUBLISH_URL = "/test/news-sharing-api/success/getRecipients" +const SUCCESS_PREVIEW_URL = "/test/news-sharing-api/success/preview" +const FAULTY_GET_RECIPIENTS_URL_404 = '/test/news-sharing-api/fail/getRecipients/404' +const FAULTY_GET_RECIPIENTS_URL_500 = '/test/news-sharing-api/fail/getRecipients/500' + +const INCORRECT_GET_RECIPIENTS_URL = '/test/news-sharing-api/fail/getRecipients' + +const SUCCESS_GET_RECIPIENTS_RESULT = [ + { + id: '1231-2312-3331-1231', + name: 'Mayview house chat', + recipients: 120, + }, + { + id: '5231-2312-3331-1233', + name: 'Bayview house chat', + recipients: 990, + }, + { + id: '5231-2312-3331-1233', + name: 'Bayview house chat', + }, +] + + +const INCORRECT_GET_RECIPIENTS_RESULT = [ + { + // Does not have name + id: '1231-2312-3331-1231', + recipients: 120, + }, + { + // Does not have ID + name: 'Bayview house chat', + recipients: 990, + }, +] + + +class NewsSharingTestingApp { + async prepareMiddleware () { + // this route can not be used for csrf attack (because no cookies and tokens are used in a public route) + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage + const app = express() + + app.get(SUCCESS_GET_RECIPIENTS_URL, async (req, res, next) => { + res.json(SUCCESS_GET_RECIPIENTS_RESULT) + }) + + app.post(SUCCESS_PUBLISH_URL, async (req, res, next) => { + res.status(200).send('OK') + }) + + app.post(SUCCESS_PREVIEW_URL, async (req, res, next) => { + res.status(200).send('OK') + }) + + app.get(INCORRECT_GET_RECIPIENTS_URL, async (req, res, next) => { + res.status(200).send(INCORRECT_GET_RECIPIENTS_RESULT) + }) + + app.get(FAULTY_GET_RECIPIENTS_URL_500, async (req, res, next) => { + res.status(500).send({}) + }) + + app.get(FAULTY_GET_RECIPIENTS_URL_404, async (req, res, next) => { + res.status(404).send({}) + }) + + return app + } +} + +module.exports = { + NewsSharingTestingApp, + SUCCESS_GET_RECIPIENTS_URL, + SUCCESS_PUBLISH_URL, + SUCCESS_PREVIEW_URL, + FAULTY_GET_RECIPIENTS_URL_404, + FAULTY_GET_RECIPIENTS_URL_500, + INCORRECT_GET_RECIPIENTS_URL, + + SUCCESS_GET_RECIPIENTS_RESULT, + INCORRECT_GET_RECIPIENTS_RESULT, +} diff --git a/apps/condo/domains/news/utils/testSchema/index.js b/apps/condo/domains/news/utils/testSchema/index.js index 762d0059cfa..f9f3799fd5d 100644 --- a/apps/condo/domains/news/utils/testSchema/index.js +++ b/apps/condo/domains/news/utils/testSchema/index.js @@ -20,6 +20,7 @@ const { const { NEWS_TYPE_COMMON } = require('@condo/domains/news/constants/newsTypes') const { FLAT_UNIT_TYPE } = require('@condo/domains/property/constants/common') const { NewsItemSharing: NewsItemSharingGQL } = require('@condo/domains/news/gql') +const { GET_NEWS_SHARING_RECIPIENTS_MUTATION } = require('@condo/domains/news/gql') /* AUTOGENERATE MARKER */ const NewsItem = generateGQLTestUtils(NewsItemGQL) @@ -323,6 +324,24 @@ async function updateTestNewsItemSharing (client, id, extraAttrs = {}) { return [obj, attrs] } + +async function getNewsSharingRecipientsByTestClient(client, b2bAppContext, extraAttrs = {}) { + if (!client) throw new Error('no client') + + if (!b2bAppContext.id) throw new Error('no b2bAppContext id') + + const sender = { dv: 1, fingerprint: faker.random.alphaNumeric(8) } + + const attrs = { + dv: 1, + sender, + b2bAppContext: { id: b2bAppContext.id }, + ...extraAttrs, + } + const { data, errors } = await client.mutate(GET_NEWS_SHARING_RECIPIENTS_MUTATION, { data: attrs }) + throwIfError(data, errors) + return [data.result, attrs] +} /* AUTOGENERATE MARKER */ module.exports = { @@ -336,5 +355,6 @@ module.exports = { getNewsItemsRecipientsCountersByTestClient, NewsItemRecipientsExportTask, createTestNewsItemRecipientsExportTask, updateTestNewsItemRecipientsExportTask, NewsItemSharing, createTestNewsItemSharing, updateTestNewsItemSharing, + getNewsSharingRecipientsByTestClient, /* AUTOGENERATE MARKER */ } diff --git a/apps/condo/index.js b/apps/condo/index.js index 2ed012187ec..a47f99f2893 100644 --- a/apps/condo/index.js +++ b/apps/condo/index.js @@ -23,6 +23,7 @@ const { getWebhookModels } = require('@open-condo/webhooks/schema') const { PaymentLinkMiddleware } = require('@condo/domains/acquiring/PaymentLinkMiddleware') const FileAdapter = require('@condo/domains/common/utils/fileAdapter') const { VersioningMiddleware } = require('@condo/domains/common/utils/VersioningMiddleware') +const { NewsSharingTestingApp } = require('@condo/domains/news/utils/testSchema/NewsSharingTestingApp') const { UnsubscribeMiddleware } = require('@condo/domains/notification/UnsubscribeMiddleware') const { UserExternalIdentityMiddleware } = require('@condo/domains/user/integration/UserExternalIdentityMiddleware') const { OIDCMiddleware } = require('@condo/domains/user/oidc') @@ -123,6 +124,11 @@ const checks = [ ] const lastApp = conf.NODE_ENV === 'test' ? undefined : new NextApp({ dir: '.' }) + +const testApps = (conf.NODE_ENV === 'test' || conf.NODE_ENV === 'development') ? [ + new NewsSharingTestingApp(), +] : [] + const apps = () => { return [ new HealthCheck({ checks }), @@ -135,6 +141,7 @@ const apps = () => { new UnsubscribeMiddleware(), FileAdapter.makeFileAdapterMiddleware(), new UserExternalIdentityMiddleware(), + ...testApps, ] } diff --git a/apps/condo/lang/en/en.json b/apps/condo/lang/en/en.json index 34f207c06d7..c01aa3bca3c 100644 --- a/apps/condo/lang/en/en.json +++ b/apps/condo/lang/en/en.json @@ -2200,6 +2200,9 @@ "api.newsItem.WRONG_SEND_DATE": "Wrong send date", "api.newsItem.EMPTY_NEWS_ITEM_SCOPE": "Empty scope", "api.newsItem.UNIT_NAME_WITHOUT_UNIT_TYPE": "Unit name set without unit type", + "api.newsItem.getNewsSharingRecipients.NOT_NEWS_SHARING_APP": "Provided b2bAppContext.app is not a NewsSharing miniapp", + "api.newsItem.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_FAILED": "Could not get a successful response from NewsSharing miniapp. Please check the network and url configuration", + "api.newsItem.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE": "Response from NewsSharing miniapp was successful, but the data format was incorrect. Please consult with miniapp developer", "api.callRecord.NEGATIVE_TALK_TIME": "Talk time must be greater or equal than 0", "api.callRecordFragment.INVALID_TICKET_ORGANIZATION": "Ticket must be from the same organization as call record", "api.newsItem.NO_NEWS_ITEM_SCOPES": "Scope-less news item publishing is forbidden", diff --git a/apps/condo/lang/ru/ru.json b/apps/condo/lang/ru/ru.json index 2a6bc989c4b..acaa0ccf56f 100644 --- a/apps/condo/lang/ru/ru.json +++ b/apps/condo/lang/ru/ru.json @@ -2200,6 +2200,9 @@ "api.newsItem.WRONG_SEND_DATE": "Неверная дата отправки", "api.newsItem.EMPTY_NEWS_ITEM_SCOPE": "Не указаны получатели", "api.newsItem.UNIT_NAME_WITHOUT_UNIT_TYPE": "Не задан unitName без unitType", + "api.newsItem.getNewsSharingRecipients.NOT_NEWS_SHARING_APP": "Этот миниапп не поддерживает функциональность отправки новостей", + "api.newsItem.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_FAILED": "Запрос в миниапп завершился с ошибкой", + "api.newsItem.getNewsSharingRecipients.NEWS_SHARING_APP_REQUEST_BAD_RESPONSE": "Запрос в миниапп вернул некорректные данные", "api.callRecord.NEGATIVE_TALK_TIME": "Длительность разговора должна быть больше или равна 0", "api.callRecordFragment.INVALID_TICKET_ORGANIZATION": "Заявка должна быть из той же организации, что и запись разговора", "api.newsItem.NO_NEWS_ITEM_SCOPES": "Нельзя публиковать новость без получателей", diff --git a/apps/condo/schema.graphql b/apps/condo/schema.graphql index 7827b8c7120..263da366958 100644 --- a/apps/condo/schema.graphql +++ b/apps/condo/schema.graphql @@ -73669,6 +73669,18 @@ type GetNewsItemsRecipientsCountersOutput { receiversCount: Int! } +input GetNewsSharingRecipientsInput { + dv: Int! + sender: JSON! + b2bAppContext: B2BAppContextWhereUniqueInput! +} + +type GetNewsSharingRecipientsOutput { + id: String! + name: String! + recipients: Int +} + enum AppCategory { DISPATCHING GIS @@ -77525,6 +77537,7 @@ type Query { getOverviewDashboard(data: GetOverviewDashboardInput!): GetOverviewDashboardOutput exportPropertyScopesToExcel(data: ExportPropertyScopeToExcelInput!): ExportPropertyScopeToExcelOutput getNewsItemsRecipientsCounters(data: GetNewsItemsRecipientsCountersInput!): GetNewsItemsRecipientsCountersOutput + getNewsSharingRecipients(data: GetNewsSharingRecipientsInput!): [GetNewsSharingRecipientsOutput] allMiniApps(data: AllMiniAppsInput!): [MiniAppOutput!] """The version of the Keystone application serving this API.""" diff --git a/apps/condo/schema.ts b/apps/condo/schema.ts index 69c9b690733..a44c7e11248 100644 --- a/apps/condo/schema.ts +++ b/apps/condo/schema.ts @@ -22921,6 +22921,19 @@ export type GetNewsItemsRecipientsCountersOutput = { receiversCount: Scalars['Int']; }; +export type GetNewsSharingRecipientsInput = { + dv: Scalars['Int']; + sender: Scalars['JSON']; + b2bAppContext: B2BAppContextWhereUniqueInput; +}; + +export type GetNewsSharingRecipientsOutput = { + __typename?: 'GetNewsSharingRecipientsOutput'; + id: Scalars['String']; + name: Scalars['String']; + recipients?: Maybe; +}; + export enum GetOverviewDashboardAggregatePeriod { Day = 'day', Week = 'week' @@ -62944,6 +62957,7 @@ export type Query = { getOverviewDashboard?: Maybe; exportPropertyScopesToExcel?: Maybe; getNewsItemsRecipientsCounters?: Maybe; + getNewsSharingRecipients?: Maybe>>; allMiniApps?: Maybe>; /** The version of the Keystone application serving this API. */ appVersion?: Maybe; @@ -69384,6 +69398,11 @@ export type QueryGetNewsItemsRecipientsCountersArgs = { }; +export type QueryGetNewsSharingRecipientsArgs = { + data: GetNewsSharingRecipientsInput; +}; + + export type QueryAllMiniAppsArgs = { data: AllMiniAppsInput; }; diff --git a/packages/keystone/test.utils.js b/packages/keystone/test.utils.js index 440506bee37..6b129f8126c 100644 --- a/packages/keystone/test.utils.js +++ b/packages/keystone/test.utils.js @@ -46,6 +46,7 @@ const TESTS_LOG_REQUEST_RESPONSE = conf.TESTS_LOG_REQUEST_RESPONSE const TESTS_LOG_FAKE_CLIENT_RESPONSE_ERRORS = conf.TESTS_FAKE_CLIENT_MODE && conf.TESTS_LOG_FAKE_CLIENT_RESPONSE_ERRORS const TESTS_LOG_REAL_CLIENT_RESPONSE_ERRORS = !conf.TESTS_FAKE_CLIENT_MODE && conf.TESTS_LOG_REAL_CLIENT_RESPONSE_ERRORS const TESTS_REAL_CLIENT_REMOTE_API_URL = conf.TESTS_REAL_CLIENT_REMOTE_API_URL || `${conf.SERVER_URL}${API_PATH}` +const TESTS_FAKE_CLIENT_EXPRESS_PORT = conf['TESTS_FAKE_CLIENT_EXPRESS_PORT'] || '3000' const SIGNIN_BY_PHONE_AND_PASSWORD_MUTATION = gql` mutation authenticateUserWithPhoneAndPassword ($phone: String!, $password: String!) { @@ -149,7 +150,7 @@ function setFakeClientMode (entryPoint, prepareKeystoneOptions = {}) { __keystone = res.keystone // tests express for a fake gql client // nosemgrep: problem-based-packs.insecure-transport.js-node.using-http-server.using-http-server - __expressServer = http.createServer(__expressApp).listen(0) + __expressServer = http.createServer(__expressApp).listen(TESTS_FAKE_CLIENT_EXPRESS_PORT) }) afterAll(async () => { if (__expressServer) __expressServer.close()