Skip to content

Commit

Permalink
feat(#238): Contact us page api (#243)
Browse files Browse the repository at this point in the history
* #238: Created a new mongodb schema for contact

* #238: Refactor error formatter to return error instead of throw

* #238: Add sinon lib for unit testing

* #238: Refactored to return error instead of throwing

* #238: Added unit testing for contact and refactored existing test

* #238: Added contact query/mutation and schema
  • Loading branch information
tholulomo authored Aug 1, 2022
1 parent 0ea1064 commit b60958b
Show file tree
Hide file tree
Showing 14 changed files with 433 additions and 22 deletions.
1 change: 1 addition & 0 deletions resfulservice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"multer": "^1.4.4",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
"sinon": "^14.0.0",
"swagger-ui-express": "^4.2.0",
"unique-names-generator": "^4.7.1",
"winston": "^3.5.1",
Expand Down
134 changes: 134 additions & 0 deletions resfulservice/spec/graphql/resolver/contact.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
const chai = require('chai');
const sinon = require('sinon');
const Contact = require('../../../src/models/contact')
const graphQlSchema = require('../../../src/graphql');
const { Mutation: { submitContact }, Query: { getUserContacts, contacts } } = require('../../../src/graphql/resolver');
const { mongoConn } = require("../../utils/mongo");

const { expect } = chai;

mongoConn();
const mockContact = {
fullName: 'test user',
email: 'test@example.com',
purpose: 'QUESTION',
message: 'test message'
}

describe('Contact Resolver Unit Tests:', function () {

afterEach(() => sinon.restore());

this.timeout(10000)

const input = {
...mockContact
}

const req = { logger: { info: (_message) => { }, error: (_message) => { } } }

context('submitContact', () => {
it('should have createContact(...) as a Mutation resolver', async function () {
const { submitContact } = graphQlSchema.getMutationType().getFields();
expect(submitContact.name).to.equal('submitContact');
expect(submitContact.type.toString()).to.equal('Contact!');
});

it('should save new contact information', async () => {
sinon.stub(Contact.prototype, 'save').callsFake(() => ({...input, _id: 'akdn9wqkn'}))

const contact = await submitContact({}, { input }, { req });

expect(contact).to.have.property('email');
});

it('should throw a 500 error', async () => {
sinon.stub(Contact.prototype, 'save').throws();

const result = await submitContact({}, { input }, { req });

expect(result).to.have.property('extensions');
expect(result.extensions.code).to.be.equal(500)
});
})

context('getUserContacts', () => {
const mockUserContact = {
_id: 'akdn9wqkn',
fullName: 'test user',
email: 'test@example.com',
purpose: 'QUESTION',
message: 'test message',
lean: () => this
}

it("should return paginated lists of a user's contacts", async () => {
sinon.stub(Contact, 'countDocuments').returns(1);
sinon.stub(Contact, 'find').returns(mockUserContact);
sinon.stub(mockUserContact, 'lean').returnsThis();

const userContacts = await getUserContacts({}, { input }, { req, isAuthenticated: true });

expect(userContacts).to.have.property('data');
expect(userContacts.totalItems).to.equal(1);
});

it("should throw a 401, not authenticated error", async () => {

const error = await getUserContacts({}, { input }, { req, isAuthenticated: false });
expect(error).to.have.property('extensions');
expect(error.extensions.code).to.be.equal(401);
});

it('should return a 500 error', async () => {
sinon.stub(Contact, 'countDocuments').throws();

const result = await getUserContacts({}, { input }, { req, isAuthenticated: true });

expect(result).to.have.property('extensions');
expect(result.extensions.code).to.be.equal(500)
});
})


context('contacts', () => {
it('should return paginated lists of contacts', async () => {
const mockContact = {
_id: 'akdn9wqkn',
fullName: 'test user',
email: 'test@example.com',
purpose: 'QUESTION',
message: 'test message',
skip: () => ({_id: 'akdn9wqkn', ...input }),
lean: () => ({_id: 'akdn9wqkn', ...input }),
limit: () => ({_id: 'akdn9wqkn', ...input })
}
sinon.stub(Contact, 'countDocuments').returns(1);
sinon.stub(Contact, 'find').returns(mockContact);
sinon.stub(mockContact, 'skip').returnsThis();
sinon.stub(mockContact, 'limit').returnsThis();
sinon.stub(mockContact, 'lean').returnsThis();

const allContacts = await contacts({}, { input, pageNumber: 1, pageSize: 1 }, { req, isAuthenticated: true });
expect(allContacts).to.have.property('data');
expect(allContacts.totalItems).to.equal(1);
});

it("should throw a 401, not authenticated error", async () => {

const error = await contacts({}, { input }, { req, isAuthenticated: false });
expect(error).to.have.property('extensions');
expect(error.extensions.code).to.be.equal(401);
});

it('should return a 500 error', async () => {
sinon.stub(Contact, 'countDocuments').returns(1);
sinon.stub(Contact, 'find').throws()

const result = await contacts({}, { input }, { req, isAuthenticated: true });

expect(result).to.have.property('extensions');
expect(result.extensions.code).to.be.equal(500)
});
})
});
38 changes: 25 additions & 13 deletions resfulservice/spec/graphql/resolver/user.spec.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
const chai = require('chai');
const mongoose = require('mongoose');
const sinon = require('sinon');
const User = require('../../../src/models/user')
const graphQlSchema = require('../../../src/graphql');
const { Mutation: { createUser } } = require('../../../src/graphql/resolver');
const { mongoConn } = require("../../utils/mongo")


const { expect } = chai;
mongoConn();

describe('Resolver Unit Tests:', function () {
// const mockUser = {
// alias: "testAlias",
// givenName: "testName",
// surName: "testSurname",
// displayName: "testDisplayName",
// email: "test@example.com"
// }

describe('User Resolver Unit Tests:', function () {

afterEach(() => sinon.restore());

this.timeout(10000)

const input = {
surName: 'surName', alias: 'alias', displayName: 'displayName', email: 'gmail88@email.com', givenName: 'givenName', key: 'key'
}

const req = { logger: { info: (_message) => { } } }

it('should have createUser(...) as a Mutation resolver', async function () {
const { createUser } = graphQlSchema.getMutationType().getFields();
expect(createUser.name).to.equal('createUser');
});

it.skip('createUser', async () => {
const user = await createUser({}, {
input: {
surName: 'surName', alias: 'alias', displayName: 'displayName', email: 'gmail88@email.com', givenName: 'givenName', key: 'key'
}
}, { req: { logger: { info: (message) => { } } } })
it('createUser', async () => {
sinon.stub(User, 'countDocuments').returns(0);
sinon.stub(User.prototype, 'save').callsFake(() => ({...input, _id: 'b39ak9qna'}))
const user = await createUser({}, { input }, { req });

console.log("hellow user", user);
// console.log(user);
expect(user).to.have.property('_id');
});

it('should return User! datatype for createUser(...) mutation', () => {
Expand Down
43 changes: 43 additions & 0 deletions resfulservice/spec/graphql/schema/contact.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const { expect } = require('chai');
const graphQlSchema = require('../../../src/graphql');

describe('Schema Unit Tests:', function () {
it('should have Query name as schema query type', async function () {
const schemaQuery = graphQlSchema.getQueryType().toString();
expect(schemaQuery).to.equal('Query');
});

it('should have Mutation name as schema mutation type', async function () {
const schemaMutation = graphQlSchema.getMutationType().toString();
expect(schemaMutation).to.equal('Mutation');
});

it('should have getSingleContact as a RootQuery field', async function () {
const { getUserContacts } = graphQlSchema.getQueryType().getFields();
expect(getUserContacts.name).to.equal('getUserContacts');
expect(getUserContacts.type.toString()).to.equal('Contacts!');
});

it('should have contacts as a RootQuery field', async function () {
const { contacts } = graphQlSchema.getQueryType().getFields();
expect(contacts.name).to.equal('contacts');
expect(contacts.type.toString()).to.equal('Contacts!');
});

it('should have _id,fullName,email,purpose,message Contact schema', async function () {
const contact = graphQlSchema.getType('Contact');
const keys = Object.keys(contact.getFields());
expect(keys).to.include.members(['_id', 'fullName', 'email', 'purpose', 'message']);
});

it('should have correct datatypes for Contact schema', async function () {
const contact = graphQlSchema.getType('Contact');
const { _id, fullName, email, purpose, message } = contact.getFields();

expect(_id.type.toString()).to.equal('ID');
expect(fullName.type.toString()).to.equal('String!');
expect(email.type.toString()).to.equal('String!');
expect(purpose.type.toString()).to.equal('Purpose!');
expect(message.type.toString()).to.equal('String!');
});
});
70 changes: 70 additions & 0 deletions resfulservice/src/graphql/resolver/contact/input.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"Input schema for fetching a single contact"
input userContactQueryInput {
"""
email of user whose contact to fetch.
This field is a required field for fetching all contacts made by a user.
"""
email: String!
}

input CreatedBetween {
from: String!
to: String!
}

"""
Input schema for for fetching a list of contacts.
Contains pagination options for how the contacts should be grouped
"""
input contactsQueryInput {
"""
Pagination option for the page of contacts to display to return.
This defaults to 1 if no value is specified.
The contacts are grouped into pages based on the specified pageSize(defaults to 20, if not specified)
"""
pageNumber: Int

"filter "
datetime: CreatedBetween

"""
Number of contacts per page.
Pagination option for specifying how the server should group the contacts.
Defaults to 1 if none is specified.
"""
pageSize: Int
}

"Input Schema type for creating contact"
input createContactInput {
"full name of the user requesting contact"
fullName: String!

"email address of the user requesting contact"
email: String!

"""
purpose for requesting contact.
You can specify only one from the list of enumerable values defined
"""
purpose: Purpose!

"details of the reason or purpose for requesting contact"
message: String!

}

"list of enumerable value selectable as purpose for requesting contact"
enum Purpose {
"I want to ask a question"
QUESTION

"I want to submit a ticket"
TICKET

"I have to make a suggestion"
SUGGESTION

"I want to make a comment"
COMMENT
}
19 changes: 19 additions & 0 deletions resfulservice/src/graphql/resolver/contact/mutation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const Contact = require('../../../models/contact');
const errorFormater = require('../../../utils/errorFormater');

const contactMutation = {
submitContact: async (_, { input }, { req }) => {
req.logger.info('createContact Function Entry:');

const contact = Contact(input);

try {
await contact.save();
return contact;
} catch (error) {
return errorFormater(error.message, 500);
}
}
};

module.exports = contactMutation;
45 changes: 45 additions & 0 deletions resfulservice/src/graphql/resolver/contact/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const Contact = require('../../../models/contact');
const errorFormater = require('../../../utils/errorFormater');
const paginator = require('../../../utils/paginator');

const contactQuery = {
getUserContacts: async (_, { input }, { user, req, isAuthenticated }) => {
req.logger?.info('[getUserContacts] Function Entry:');
if (!isAuthenticated) {
req.logger?.error('[contacts]: User not authenticated to view contact listing');
return errorFormater('not authenticated', 401);
}
try {
const pagination = paginator(await Contact.countDocuments(input));
const data = await Contact.find(input).lean();
return Object.assign(pagination, { data });
} catch (error) {
return errorFormater(error.message, 500);
}
},

contacts: async (_, { input }, { user, req, isAuthenticated }) => {
req.logger?.info('[contacts]: Function Entry:');
if (!isAuthenticated) {
req.logger?.error('[contacts]: User not authenticated to view contact listing');
return errorFormater('not authenticated', 401);
}
const filter = input?.datetime?.from && input?.datetime?.to
? {
createdAt: {
$gt: new Date(input.datetime.from),
$lt: new Date(input.datetime.to)
}
}
: {};
try {
const pagination = input ? paginator(await Contact.countDocuments({}), input.pageNumber, input.pageSize) : paginator(await Contact.countDocuments({}));
const data = await Contact.find(filter).skip(pagination.skip).limit(pagination.limit).lean();
return Object.assign(pagination, { data });
} catch (error) {
return errorFormater(error.message, 500);
}
}
};

module.exports = contactQuery;
Loading

0 comments on commit b60958b

Please sign in to comment.