Skip to content

Commit

Permalink
fix(condo) DOMA-7397 generate initial query files
Browse files Browse the repository at this point in the history
fix(condo) DOMA-7397 finish GetNewsSharingRecipientsService code

fix(condo) DOMA-7397 learn how to launch query

fix(condo) DOMA-7397 add example api

fix(condo) DOMA-7397 add TESTS_FAKE_CLIENT_EXPRESS_PORT variable

fix(condo) DOMA-7397 add new error

fix(condo) DOMA-7397 fix more tests

fix(condo) DOMA-7397 figure out translations

fix(condo) DOMA-7397 figure out graphql types

fix(condo) DOMA-7397 figure out tests

fix(condo) DOMA-7397 maketypes
  • Loading branch information
toplenboren committed Mar 8, 2024
1 parent e284780 commit a19b2f5
Show file tree
Hide file tree
Showing 14 changed files with 540 additions and 1 deletion.
31 changes: 31 additions & 0 deletions apps/condo/domains/news/access/GetNewsSharingRecipientsService.js
Original file line number Diff line number Diff line change
@@ -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,
}
7 changes: 7 additions & 0 deletions apps/condo/domains/news/gql.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <CONST> */

module.exports = {
Expand All @@ -46,5 +52,6 @@ module.exports = {
GET_NEWS_ITEMS_RECIPIENTS_COUNTERS_MUTATION,
NewsItemRecipientsExportTask,
NewsItemSharing,
GET_NEWS_SHARING_RECIPIENTS_MUTATION,
/* AUTOGENERATE MARKER <EXPORTS> */
}
159 changes: 159 additions & 0 deletions apps/condo/domains/news/schema/GetNewsSharingRecipientsService.js
Original file line number Diff line number Diff line change
@@ -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 <URL>?organizationId=string
*
* Where:
* - URL = getRecipeints endpoint url provided in NewsItemSharingConfig
*
* Response payload:
* [{
* id: <string>
* subscribers: <number>
* name: <string>
* },
* {...}
* ]
*
* GetRecipients endpoint should be covered behind BASIC auth
*
* --
*
* This method is just a proxy between Condo and miniapp.
**
* <Condo /news> -> <Condo.GetNewsSharingRecipientsService> -> BASIC-AUTH -> <Miniapp.GetRecipients>
*/

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,
}
170 changes: 170 additions & 0 deletions apps/condo/domains/news/schema/GetNewsSharingRecipientsService.test.js
Original file line number Diff line number Diff line change
@@ -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')

Check failure on line 14 in apps/condo/domains/news/schema/GetNewsSharingRecipientsService.test.js

View workflow job for this annotation

GitHub Actions / Lint source code

A space is required before '}'
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)
})
})
})
2 changes: 2 additions & 0 deletions apps/condo/domains/news/schema/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

const { GetNewsItemsRecipientsCountersService } = require('./GetNewsItemsRecipientsCountersService')
const { GetNewsSharingRecipientsService } = require('./GetNewsSharingRecipientsService')
const { NewsItem } = require('./NewsItem')
const { NewsItemRecipientsExportTask } = require('./NewsItemRecipientsExportTask')
const { NewsItemScope } = require('./NewsItemScope')
Expand All @@ -20,5 +21,6 @@ module.exports = {
GetNewsItemsRecipientsCountersService,
NewsItemRecipientsExportTask,
NewsItemSharing,
GetNewsSharingRecipientsService,
/* AUTOGENERATE MARKER <EXPORTS> */
}
Loading

0 comments on commit a19b2f5

Please sign in to comment.