Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write events test suite #181

Closed
wants to merge 35 commits into from
Closed

Write events test suite #181

wants to merge 35 commits into from

Conversation

shravanhariharan2
Copy link
Collaborator

Closes #175

@github-actions
Copy link

Thanks for contributing! If you've made changes to the API's functionality, please make sure to bump the package version—see this guide to semantic versioning for details—and document those changes as appropriate.

Copy link
Member

@sumeet-bansal sumeet-bansal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. I think we should mock StorageService instead of actually writing/reading to an S3 bucket (and definitely not to the same folder that prod uses), and we should start passing in our configs to constructors to make it easier to mock things.

@@ -18,7 +18,7 @@ export default class EventService {
public async create(event: Event) {
const eventCreated = await this.transactions.readWrite(async (txn) => {
const eventRepository = Repositories.event(txn);
const isUnusedAttendanceCode = eventRepository.isUnusedAttendanceCode(event.attendanceCode);
const isUnusedAttendanceCode = await eventRepository.isUnusedAttendanceCode(event.attendanceCode);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you catch this with a test or just notice it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was caught with test - I saw that the duplicate attendance code error was thrown by Postgres' duplicate key constraint and not by the event service

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, our first bug caught by testing!

return EventFactory.fakeWithTime(start, end);
}

private static fakeWithTime(start: Date, end: Date): EventModel {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the with method instead of fakeWithTime, e.g.

private static fakeWithTime(start: Date, end: Date): EventModel {
  return with({ start, end });
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally forgot about with() - we actually don't even need fakeWithTime or fakeWithoutTime anymore if with() is used for fakePastEvent(), fakeOngoingEvent(), and fakeFutureEvent().

return event;
}

public static fakeWithoutTime(): EventModel {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the different between this and fake? Or does this method exist because of fakeWithTime?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, this method is not needed anymore

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But the idea before was to have fake, fakePastEvent, fakeOngoingEvent, fakeFutureEvent to all call fakeWithTime, but specifying different times for each section. fakeWithTime would then call fakeWithoutTime to get a random object for all other fields

return [start.toDate(), end.toDate()];
}

private static randomPastTime(daysAgo: number): [Date, Date] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we reuse code between these randomTime methods? Maybe we have optional params where we can set some days, hour, and duration fields?

@@ -19,6 +19,10 @@ export class FeedbackFactory {
};
}

public static createEventFeedback(n: number): string[] {
return new Array(n).fill(faker.random.word());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a random word or some value from a predefined array of values? IIRC feedback's supposed to be some predefined emojis.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the predefined emojis have been confirmed yet since a lot of the design materials are moving around right now, and membership hasn't acknowledged them either since the portal redesign has been pushed back significantly (till after the store frontend is finished). I think it's fine to keep them at random words for now and then adjust later once the set of emojis /text is confirmed.

.createEvents([event])
.write();

const eventCoverResponse = await ControllerFactory.event(conn).updateEventCover(cover, event.uuid, admin);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think our tests should write to our prod bucket. At least, definitely not to the same folder. This is what I said a while back, that we should pass in our configs through the constructor, so we can specify where things get written to instead of being tied to whatever's in the prod config.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure what we should pass in to the StorageService constructor, since I believe most of our config (such as bucket to be written to) is unchangeable. The S3_BUCKET, S3_REGION, and S3_CREDENTIALS are all constant through the entire app, and there's separate buckets for local (acmucsd-local, for what is used for locally running the test suite and in CircleCI jobs), testing (acmucsd-membership-portal, for testing.api.acmucsd.com), and prod (acmucsd, for what is used in production), so tests don't actually get written to our prod bucket.

const ONE_BYTE_FILLER = 'a';

// Factory for mocking in-memory files for testing with multer
export class FileFactory {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do test that we're writing/reading from S3 should we also verify the file is exactly the same? If so, maybe we write a random string and verify checksums? I don't think we need to actually interact with S3 though, mocking should be good enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, mocking should be good enough.

await ControllerFactory.event(conn).submitEventFeedback(event.uuid, { feedback }, user);

const attendanceResponse = await ControllerFactory.attendance(conn).getAttendancesForCurrentUser(user);
const attendance = attendanceResponse.attendances[0];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: inline.

tests/event.test.ts Outdated Show resolved Hide resolved
.write();

const eventController = ControllerFactory.event(conn);
const errorMessage = 'Attendance code has already been used';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: inline this and the other error messages.

Copy link
Contributor

@michl1001 michl1001 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall though I'm not as intimately familiar with our test suite (whether there are better functions to use) and the higher-level design

@shravanhariharan2
Copy link
Collaborator Author

@sumeet-bansal how do you think we should go about mocking StorageService and passing in config into the constructor? To my understanding we can't just wrap that service like we do with controllers since the service would still upload to S3 and I don't think we wanted that, and I'm still not really sure what we'd pass in to the constructor of StorageService since all the config is the same except the bucket, which is changed within the environment variables.

@sumeet-bansal
Copy link
Member

What do you mean by wrapping controllers? Jest should have some sort of mocking functionality where we can do something like

const storageService = mock(IStorageService.class);
when(storageService.upload(any(), any(), eq(MediaType.EVENT_COVER), eq("some-uuid")).thenReturn("fileLocation");

(That's the syntax for the Mockito library in Java.)

But the idea is, we create a "mock" and then we can define custom responses based on the parameters, and instead of executing normally, the mock returns our defined response.

@sumeet-bansal
Copy link
Member

This looks like what we need (link).

@sumeet-bansal
Copy link
Member

Mock StorageService not the S3 library.

@shravanhariharan2
Copy link
Collaborator Author

Wait, why mock StorageService? Isn't that something we'd want to actually test, but just not have anything uploaded? My thinking was that we don't want anything uploaded, so we just replace the this.s3.upload call with a different function that doesn't upload anything, but that still returns the URL that would be returned by aws-sdk.

@shravanhariharan2
Copy link
Collaborator Author

I might just be generally confused of the concept/use case of mocking cause I think I've been the word throwing around here and there with the controllers without knowing if that's what we're actually doing with them.

@sumeet-bansal
Copy link
Member

No you're right, we could mock aws-sdk. S3Mock looks more difficult than it needs to be but maybe that's just because of jest. There isn't much of a difference between mocking aws-sdk and StorageService functionality-wise (we wouldn't verify the upload path but I don't think that's a big deal), so I figured it'd be easier to just mock StorageService instead.

@shravanhariharan2
Copy link
Collaborator Author

S3Mock looks more difficult than it needs to be but maybe that's just because of jest.

Yeah it's like that since that's how jest does mocking for modules. If we were to mock StorageService instead, then all we can assert is just that event.cover exists right? Instead of being able to check that event covers are uploaded to the event path instead of banner path, etc. (since that was also a bug that happened earlier).

@sumeet-bansal
Copy link
Member

sumeet-bansal commented May 27, 2021

  1. Yikes, maybe we should look into a better library for mocking. Looks like there's a TypeScript version of Mockito: https://github.com/NagRock/ts-mockito!
  2. We could separately test the static method that takes an enum and returns the upload info. Normally I'd say let's mock aws-sdk but StorageService is just a wrapper and the only thing we'd be testing is a really simple switch statement.

Comment on lines 63 to 65
if (changes.attendanceCode !== currentEvent.attendanceCode) {
const isUnusedAttendanceCode = eventRepository.isUnusedAttendanceCode(changes.attendanceCode);
const isUnusedAttendanceCode = await eventRepository.isUnusedAttendanceCode(changes.attendanceCode);
if (!isUnusedAttendanceCode) throw new UserError('Attendance code has already been used');
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

package.json Outdated
@@ -56,6 +56,7 @@
"pg": "^8.6.0",
"reflect-metadata": "^0.1.13",
"routing-controllers": "^0.9.0",
"ts-mockito": "^2.6.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: dev dependency.

Comment on lines 1 to 17
import { Connection } from 'typeorm';
import FeedbackService from '../../services/FeedbackService';
import { FeedbackController } from '../../api/controllers/FeedbackController';
import { UserController } from '../../api/controllers/UserController';
import UserAccountService from '../../services/UserAccountService';
import StorageService from '../../services/StorageService';
import { AdminController } from '../../api/controllers/AdminController';
import AttendanceService from '../../services/AttendanceService';
import { AttendanceController } from '../../api/controllers/AttendanceController';
import { AuthController } from '../../api/controllers/AuthController';
import { EventController } from '../../api/controllers/EventController';
import { LeaderboardController } from '../../api/controllers/LeaderboardController';
import { MerchStoreController } from '../../api/controllers/MerchStoreController';
import UserAuthService from '../../services/UserAuthService';
import EmailService from '../../services/EmailService';
import EventService from '../../services/EventService';
import MerchStoreService from '../../services/MerchStoreService';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: can we clean these imports up a bit in a follow-up PR? Let's import by api/controllers and services.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, just created an issue for it

import EventService from '../../services/EventService';
import MerchStoreService from '../../services/MerchStoreService';

export class ControllerFactory {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is getting messy. Let's remove the controller singletons and initialize a new controller each time the factory's called.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Is the reason we are willing to trade-off readability for memory because this class is used only in tests and isn't persisting any memory afterwards? (meaning we are okay using a small amount more of memory since it's all present only during the tests running)

import EventService from '../../services/EventService';
import MerchStoreService from '../../services/MerchStoreService';

export class ControllerFactory {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: rename file to the class name.

@@ -24,11 +23,6 @@ export class Feedback implements IFeedback {
type: FeedbackType;
}

export class SubmitEventFeedbackRequest implements ISubmitEventFeedbackRequest {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this change for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved SubmitEventFeedbackRequest from FeedbackControllerRequests to EventControllerRequests

const attendanceResponse = await ControllerFactory.attendance(conn).getAttendancesForCurrentUser(user);

expect(attendanceResponse.attendances[0].feedback).toEqual(feedback);
expect(user.points).toEqual(event.pointValue + Config.pointReward.EVENT_FEEDBACK_POINT_REWARD);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this passing? Shouldn't we need to refresh the UserModel before the points field is updated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/acmucsd/membership-portal/blob/master/repositories/UserRepository.ts#L52

This line updates the points field directly on the user object, so no refresh is necessary.

Comment on lines +244 to +246
expect(errors).toBeDefined();
expect(errors).toHaveLength(1);
expect(errors[0].property).toEqual('feedback');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an expect that checks that the reason validation failed is because >3?

const uuid = submittedFeedbackResponse.feedback;
const acknowledgedFeedback = await feedbackController.updateFeedbackStatus(uuid, { status }, admin);
const { uuid } = submittedFeedbackResponse.feedback;
const acknowledgedFeedback = await feedbackController.updateFeedbackStatus({ uuid }, { status }, admin);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any changes to the feedback controller so why this change?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it was a missing change from this PR that wasn't causing the test to fail for some reason.

import StorageService from '../../services/StorageService';

export default class Mocks {
public static storage(fileLocation: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline this and use verify to check that we called the mock correctly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure what you mean by this. What should we be inlining, and where should we be verifying the call?

.event(conn, null, Mocks.storage(fileLocation))
.updateEventCover(cover, { uuid: event.uuid }, admin);

expect(eventCoverResponse.event.cover).toEqual(fileLocation);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should get the event and check the cover from that instead of checking the updateEventCoverResponse.

StormFireFox1 pushed a commit that referenced this pull request Jul 17, 2021
Code from #181 adds ability to fake events in the past, future or present.
Taken verbatim from commit 18a952c.
StormFireFox1 added a commit that referenced this pull request Aug 2, 2021
* Add tests for "/bonus" and "/emails" routes

Squashed set of commits for 6f51db8...c70e444. Merge conflict
was on package-lock, which was hard to fix, so I just redid the commits.

* nit: "proxyUser" -> "adminUser" for admin tests

* Adjust array equality checks on admin tests

* Remove test for non-present emails

Test is not necessary due to lack of filtering in original controller.

* Inline functions and variables in admin tests

* Modify test to check bonus points for all users

* Combine both bonus points tests into one

Tests had basically the same boilerplate. Combine them to check both
conditions at once.

The test already checked if we were adding bonus point to _specifically_
the emails we passed in, so it's best to also check if an extra user
wasn't included at the same time.

* Rearrange asserts to suffix response calls

* Rename adminUser to admin in admin test suite

* Adjust order of assertions in admin test

* Adjust name of extraneous user in admin test

* Make variable for UserController in admin test

Inlining UserController made one line too long.
Since it is used multiple times in the test, extract
variable to make its usage less verbose.

* Fix nits and check attendance response details

* Check for correct activities in admin test suite

* Add points field of ActivityModel in test suite

ActivityModel is missing the `pointsEarned` field whenever creating
an Activity after attending an event. This only happens in our mock
tester.

Add a check for this field's presence, along with fixing the inherent
bug within the mock state.

* Port EventFactory code from #181

Code from #181 adds ability to fake events in the past, future or present.
Taken verbatim from commit 18a952c.

* Adjust test suite to account for factory bug

Account creation can occur after event attendance, causing activities
to be out of order chronologically. Guarantee order in activities by
introducing retroactive attendance in a future event.

Bug in factory is not a reproducible issue in production, since account
creation will always occur before event attendance in real time.

* Revert event factory code changes

* Adjust transaction order when saving factory state

Mock factory `write` function incorrectly saves attendance before
any possible activities, typically causing event attendances to be
stored in the database prior to activities being stored. Adjust order
of flushing to database in order to consistently set smaller timestamps
for account-related activities, such as account creation.

* Revert transaction order adjustment

This reverts commit b5f03dc.

Tests continue to be non-deterministic despite this change. My
assumption was wrong.

* Guarantee correct activity order in failed tests

Create users at beginning of the Unix epoch to ensure guarantee
of event attendance activity being second is fulfilled.

Since Activities are sorted by timestamp, we need to guarantee account
creation does not occur before attendance of event. Fix issue with above
workaround, providing explicit method whenever necessary.

* Merge user creation methods to have optional args

Rather than having two explicit methods, merge methods and use optional
timestamp argument to allow for custom creation of users at explicit times.

* Adjust user creation method to have set timestamp

Use specified timestamp of 1 month before current time to place account
creation in time before most activities for testing.

* Fix argument call for creating users in test suite

Co-authored-by: Matei-Alexandru Gardus <[email protected]>
@shravanhariharan2
Copy link
Collaborator Author

shravanhariharan2 commented Sep 18, 2021

Closing this for now since this will not be worked on for quite a bit, as merch store issues are being worked on for the October launch. I'll reopen this once things have settled with the store.

This branch should not be deleted

nick-ls pushed a commit to nick-ls/membership-portal that referenced this pull request Aug 6, 2024
* Add tests for "/bonus" and "/emails" routes

Squashed set of commits for 6f51db8...c70e444. Merge conflict
was on package-lock, which was hard to fix, so I just redid the commits.

* nit: "proxyUser" -> "adminUser" for admin tests

* Adjust array equality checks on admin tests

* Remove test for non-present emails

Test is not necessary due to lack of filtering in original controller.

* Inline functions and variables in admin tests

* Modify test to check bonus points for all users

* Combine both bonus points tests into one

Tests had basically the same boilerplate. Combine them to check both
conditions at once.

The test already checked if we were adding bonus point to _specifically_
the emails we passed in, so it's best to also check if an extra user
wasn't included at the same time.

* Rearrange asserts to suffix response calls

* Rename adminUser to admin in admin test suite

* Adjust order of assertions in admin test

* Adjust name of extraneous user in admin test

* Make variable for UserController in admin test

Inlining UserController made one line too long.
Since it is used multiple times in the test, extract
variable to make its usage less verbose.

* Fix nits and check attendance response details

* Check for correct activities in admin test suite

* Add points field of ActivityModel in test suite

ActivityModel is missing the `pointsEarned` field whenever creating
an Activity after attending an event. This only happens in our mock
tester.

Add a check for this field's presence, along with fixing the inherent
bug within the mock state.

* Port EventFactory code from acmucsd#181

Code from acmucsd#181 adds ability to fake events in the past, future or present.
Taken verbatim from commit 18a952c.

* Adjust test suite to account for factory bug

Account creation can occur after event attendance, causing activities
to be out of order chronologically. Guarantee order in activities by
introducing retroactive attendance in a future event.

Bug in factory is not a reproducible issue in production, since account
creation will always occur before event attendance in real time.

* Revert event factory code changes

* Adjust transaction order when saving factory state

Mock factory `write` function incorrectly saves attendance before
any possible activities, typically causing event attendances to be
stored in the database prior to activities being stored. Adjust order
of flushing to database in order to consistently set smaller timestamps
for account-related activities, such as account creation.

* Revert transaction order adjustment

This reverts commit b5f03dc.

Tests continue to be non-deterministic despite this change. My
assumption was wrong.

* Guarantee correct activity order in failed tests

Create users at beginning of the Unix epoch to ensure guarantee
of event attendance activity being second is fulfilled.

Since Activities are sorted by timestamp, we need to guarantee account
creation does not occur before attendance of event. Fix issue with above
workaround, providing explicit method whenever necessary.

* Merge user creation methods to have optional args

Rather than having two explicit methods, merge methods and use optional
timestamp argument to allow for custom creation of users at explicit times.

* Adjust user creation method to have set timestamp

Use specified timestamp of 1 month before current time to place account
creation in time before most activities for testing.

* Fix argument call for creating users in test suite

Co-authored-by: Matei-Alexandru Gardus <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Write event controller test suite
3 participants