diff --git a/.eslintrc.js b/.eslintrc.js
index 5aef503a..61ba3f32 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -4,6 +4,7 @@ module.exports = {
es2021: true,
node: true,
browser: true,
+ "cypress/globals": true
},
extends: ["eslint:recommended", "plugin:react/recommended", "prettier"],
parserOptions: {
@@ -16,7 +17,7 @@ module.exports = {
ecmaVersion: "latest",
sourceType: "module",
},
- plugins: ["react", "react-hooks", "prettier"],
+ plugins: ["react", "react-hooks", "prettier", "cypress"],
rules: {
indent: [
"error",
diff --git a/.github/workflows/review-pull-request.yml b/.github/workflows/review-pull-request.yml
new file mode 100644
index 00000000..660c1b2a
--- /dev/null
+++ b/.github/workflows/review-pull-request.yml
@@ -0,0 +1,51 @@
+name: Review Pull Request
+
+on:
+
+ push:
+ branches:
+ - main
+ - development
+ pull_request:
+ branches:
+ - main
+ - development
+
+permissions:
+ checks: write
+ contents: write
+
+jobs:
+ review-pull-request:
+ name: Run tests to review pull requests
+ timeout-minutes: 10
+ runs-on: ubuntu-latest
+ environment: DEV
+ env:
+ DB_STRING: ${{ vars.DB_STRING }}
+ DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }}
+ DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
+ OAUTH_REDIRECT_URL: ${{ vars.OAUTH_REDIRECT_URL }}
+ MOCK_USER: ${{ vars.MOCK_USER }}
+ NODE_ENV: ${{ vars.NODE_ENV }}
+
+ steps:
+ - name: Check out Git repository
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v1
+ with:
+ node-version: 16.15.0
+
+ - name: Install Node.js dependencies
+ run: npm ci
+
+ - name: Run linters
+ uses: wearerequired/lint-action@v2
+ with:
+ eslint: true
+
+ - name: Run Front end test
+ run: npm run test-frontend
+
diff --git a/.gitignore b/.gitignore
index 9285087b..c9bff93e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,5 +3,9 @@ node_modules
.vscode
build
.DS_Store
-.mongo
+.mongo*
formLogic.txt
+coverage
+.nyc_output
+cypress/screenshots
+cypress/videos
\ No newline at end of file
diff --git a/client/package.json b/client/package.json
index d7c086e4..7593b1bf 100644
--- a/client/package.json
+++ b/client/package.json
@@ -2,6 +2,7 @@
"proxy": "http://localhost:2121",
"scripts": {
"start": "react-scripts start",
+ "start:coverage": "react-scripts -r @cypress/instrument-cra start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
@@ -31,5 +32,6 @@
"tailwindcss": "^3.2.2",
"axios": "^1.1.3",
"nanoid": "^4.0.0"
- }
+ },
+ "cypressWebpackConfigPath": "../node_modules/react-scripts/config/webpack.config.js"
}
diff --git a/client/public/index.html b/client/public/index.html
index 4605b518..b437a057 100644
--- a/client/public/index.html
+++ b/client/public/index.html
@@ -13,7 +13,7 @@
PDT nonrecurring
+// Sunday, March 12, 2023, 2:00:00 AM clocks are turned forward 1 hour to March 12, 2023, 3:00:00 AM
+// 2 AM - 3 AM doesn't exist
+const PSTtoPDTnonrecurring = {
+ timeZone: "America/Los_Angeles",
+ dates: ["2023-03-12"],
+ times() {
+ const [d] = this.dates;
+ // prettier-ignore
+ return [
+ [[ "01:30", "01:45", Date.parse(`${d}T01:30:00.000-08:00`), Date.parse(`${d}T01:45:00.000-08:00`) ]],
+ [[ "01:30", "02:00", Date.parse(`${d}T01:30:00.000-08:00`), Date.parse(`${d}T03:00:00.000-07:00`) ]],
+ [[ "01:30", "02:30", Date.parse(`${d}T01:30:00.000-08:00`), Date.parse(`${d}T03:30:00.000-07:00`) ]],
+ [[ "01:30", "03:00", Date.parse(`${d}T01:30:00.000-08:00`), Date.parse(`${d}T04:00:00.000-07:00`) ]],
+ [[ "01:30", "03:30", Date.parse(`${d}T01:30:00.000-08:00`), Date.parse(`${d}T04:30:00.000-07:00`) ]],
+ [[ "02:00", "02:30", Date.parse(`${d}T03:00:00.000-07:00`), Date.parse(`${d}T03:30:00.000-07:00`) ]],
+ [[ "02:00", "03:00", Date.parse(`${d}T03:00:00.000-07:00`), Date.parse(`${d}T04:00:00.000-07:00`) ]],
+ [[ "02:00", "03:30", Date.parse(`${d}T03:00:00.000-07:00`), Date.parse(`${d}T04:30:00.000-07:00`) ]],
+ [[ "02:30", "02:45", Date.parse(`${d}T03:30:00.000-07:00`), Date.parse(`${d}T03:45:00.000-07:00`) ]],
+ [[ "02:30", "03:00", Date.parse(`${d}T03:30:00.000-07:00`), Date.parse(`${d}T04:00:00.000-07:00`) ]],
+ [[ "02:30", "03:30", Date.parse(`${d}T03:30:00.000-07:00`), Date.parse(`${d}T04:30:00.000-07:00`) ]],
+ [[ "03:00", "03:30", Date.parse(`${d}T03:00:00.000-07:00`), Date.parse(`${d}T03:30:00.000-07:00`) ]],
+ ]
+ },
+};
+
+const PSTtoPDTrecurring = {
+ timeZone: "America/Los_Angeles",
+ recurring: { rate: "weekly", days: ["7"] },
+ groupId: "1234",
+ dates: ["2023-03-05", "2023-03-12", "2023-03-19"],
+ times() {
+ const [d1, d2, d3] = this.dates;
+ // prettier-ignore
+ return [
+ [
+ [ "01:30", "01:45", Date.parse(`${d1}T01:30:00.000-08:00`), Date.parse(`${d1}T01:45:00.000-08:00`) ],
+ [ "01:30", "01:45", Date.parse(`${d2}T01:30:00.000-08:00`), Date.parse(`${d2}T01:45:00.000-08:00`) ],
+ [ "01:30", "01:45", Date.parse(`${d3}T01:30:00.000-07:00`), Date.parse(`${d3}T01:45:00.000-07:00`) ],
+ ],
+ [
+ [ "01:30", "02:00", Date.parse(`${d1}T01:30:00.000-08:00`), Date.parse(`${d1}T02:00:00.000-08:00`) ],
+ [ "01:30", "02:00", Date.parse(`${d2}T01:30:00.000-08:00`), Date.parse(`${d2}T03:00:00.000-07:00`) ],
+ [ "01:30", "02:00", Date.parse(`${d3}T01:30:00.000-07:00`), Date.parse(`${d3}T02:00:00.000-07:00`) ],
+ ],
+ [
+ [ "01:30", "02:30", Date.parse(`${d1}T01:30:00.000-08:00`), Date.parse(`${d1}T02:30:00.000-08:00`) ],
+ [ "01:30", "02:30", Date.parse(`${d2}T01:30:00.000-08:00`), Date.parse(`${d2}T03:30:00.000-07:00`) ],
+ [ "01:30", "02:30", Date.parse(`${d3}T01:30:00.000-07:00`), Date.parse(`${d3}T02:30:00.000-07:00`) ],
+ ],
+ [
+ [ "01:30", "03:00", Date.parse(`${d1}T01:30:00.000-08:00`), Date.parse(`${d1}T03:00:00.000-08:00`) ],
+ [ "01:30", "03:00", Date.parse(`${d2}T01:30:00.000-08:00`), Date.parse(`${d2}T04:00:00.000-07:00`) ],
+ [ "01:30", "03:00", Date.parse(`${d3}T01:30:00.000-07:00`), Date.parse(`${d3}T03:00:00.000-07:00`) ],
+ ],
+ [
+ [ "01:30", "03:30", Date.parse(`${d1}T01:30:00.000-08:00`), Date.parse(`${d1}T03:30:00.000-08:00`) ],
+ [ "01:30", "03:30", Date.parse(`${d2}T01:30:00.000-08:00`), Date.parse(`${d2}T04:30:00.000-07:00`) ],
+ [ "01:30", "03:30", Date.parse(`${d3}T01:30:00.000-07:00`), Date.parse(`${d3}T03:30:00.000-07:00`) ],
+ ],
+ [
+ [ "02:00", "02:30", Date.parse(`${d1}T02:00:00.000-08:00`), Date.parse(`${d1}T02:30:00.000-08:00`) ],
+ [ "02:00", "02:30", Date.parse(`${d2}T03:00:00.000-07:00`), Date.parse(`${d2}T03:30:00.000-07:00`) ],
+ [ "02:00", "02:30", Date.parse(`${d3}T02:00:00.000-07:00`), Date.parse(`${d3}T02:30:00.000-07:00`) ],
+ ],
+ [
+ [ "02:00", "03:00", Date.parse(`${d1}T02:00:00.000-08:00`), Date.parse(`${d1}T03:00:00.000-08:00`) ],
+ [ "02:00", "03:00", Date.parse(`${d2}T03:00:00.000-07:00`), Date.parse(`${d2}T04:00:00.000-07:00`) ],
+ [ "02:00", "03:00", Date.parse(`${d3}T02:00:00.000-07:00`), Date.parse(`${d3}T03:00:00.000-07:00`) ],
+ ],
+ [
+ [ "02:00", "03:30", Date.parse(`${d1}T02:00:00.000-08:00`), Date.parse(`${d1}T03:30:00.000-08:00`) ],
+ [ "02:00", "03:30", Date.parse(`${d2}T03:00:00.000-07:00`), Date.parse(`${d2}T04:30:00.000-07:00`) ],
+ [ "02:00", "03:30", Date.parse(`${d3}T02:00:00.000-07:00`), Date.parse(`${d3}T03:30:00.000-07:00`) ],
+ ],
+ [
+ [ "02:30", "02:45", Date.parse(`${d1}T02:30:00.000-08:00`), Date.parse(`${d1}T02:45:00.000-08:00`) ],
+ [ "02:30", "02:45", Date.parse(`${d2}T03:30:00.000-07:00`), Date.parse(`${d2}T03:45:00.000-07:00`) ],
+ [ "02:30", "02:45", Date.parse(`${d3}T02:30:00.000-07:00`), Date.parse(`${d3}T02:45:00.000-07:00`) ],
+ ],
+ [
+ [ "02:30", "03:00", Date.parse(`${d1}T02:30:00.000-08:00`), Date.parse(`${d1}T03:00:00.000-08:00`) ],
+ [ "02:30", "03:00", Date.parse(`${d2}T03:30:00.000-07:00`), Date.parse(`${d2}T04:00:00.000-07:00`) ],
+ [ "02:30", "03:00", Date.parse(`${d3}T02:30:00.000-07:00`), Date.parse(`${d3}T03:00:00.000-07:00`) ],
+ ],
+ [
+ [ "02:30", "03:30", Date.parse(`${d1}T02:30:00.000-08:00`), Date.parse(`${d1}T03:30:00.000-08:00`) ],
+ [ "02:30", "03:30", Date.parse(`${d2}T03:30:00.000-07:00`), Date.parse(`${d2}T04:30:00.000-07:00`) ],
+ [ "02:30", "03:30", Date.parse(`${d3}T02:30:00.000-07:00`), Date.parse(`${d3}T03:30:00.000-07:00`) ],
+ ],
+ [
+ [ "03:00", "03:30", Date.parse(`${d1}T03:00:00.000-08:00`), Date.parse(`${d1}T03:30:00.000-08:00`) ],
+ [ "03:00", "03:30", Date.parse(`${d2}T03:00:00.000-07:00`), Date.parse(`${d2}T03:30:00.000-07:00`) ],
+ [ "03:00", "03:30", Date.parse(`${d3}T03:00:00.000-07:00`), Date.parse(`${d3}T03:30:00.000-07:00`) ],
+ ],
+ ]
+ },
+};
+
+// Sunday, November 5, 2023, 2:00:00 AM clocks are turned backward 1 hour to Sunday, November 5, 2023, 1:00:00 AM
+// 1 AM - 2 AM occurs twice
+// All event durations are saved according to the time on the clock.
+// It might be not possible to create an event during the second occurrence of 1 - 2 AM
+const PDTtoPSTnonrecurring = {
+ timeZone: "America/Los_Angeles",
+ dates: ["2023-11-05"],
+ times() {
+ const [d] = this.dates;
+ // prettier-ignore
+ return [
+ [[ "00:30", "00:45", Date.parse(`${d}T00:30:00.000-07:00`), Date.parse(`${d}T00:45:00.000-07:00`) ]],
+ [[ "00:30", "01:00", Date.parse(`${d}T00:30:00.000-07:00`), Date.parse(`${d}T01:00:00.000-07:00`) ]],
+ [[ "00:30", "01:30", Date.parse(`${d}T00:30:00.000-07:00`), Date.parse(`${d}T01:30:00.000-07:00`) ]],
+ [[ "00:30", "02:00", Date.parse(`${d}T00:30:00.000-07:00`), Date.parse(`${d}T01:00:00.000-08:00`) ]],
+ [[ "00:30", "02:30", Date.parse(`${d}T00:30:00.000-07:00`), Date.parse(`${d}T01:30:00.000-08:00`) ]],
+ [[ "01:00", "01:30", Date.parse(`${d}T01:00:00.000-07:00`), Date.parse(`${d}T01:30:00.000-07:00`) ]],
+ [[ "01:00", "02:00", Date.parse(`${d}T01:00:00.000-07:00`), Date.parse(`${d}T01:00:00.000-08:00`) ]],
+ [[ "01:00", "02:30", Date.parse(`${d}T01:00:00.000-07:00`), Date.parse(`${d}T01:30:00.000-08:00`) ]],
+ [[ "01:30", "01:45", Date.parse(`${d}T01:30:00.000-07:00`), Date.parse(`${d}T01:45:00.000-07:00`) ]],
+ [[ "01:30", "02:00", Date.parse(`${d}T01:30:00.000-07:00`), Date.parse(`${d}T01:00:00.000-08:00`) ]],
+ [[ "01:30", "02:30", Date.parse(`${d}T01:30:00.000-07:00`), Date.parse(`${d}T01:30:00.000-08:00`) ]],
+ [[ "02:00", "02:30", Date.parse(`${d}T02:00:00.000-08:00`), Date.parse(`${d}T02:30:00.000-08:00`) ]],
+ ]
+ },
+};
+
+// WEST -> WET recurring event
+const PDTtoPSTrecurring = {
+ timeZone: "America/Los_Angeles",
+ recurring: { rate: "weekly", days: ["7"] },
+ groupId: "1234",
+ dates: ["2023-10-29", "2023-11-05", "2023-11-12"],
+ times() {
+ const [d1, d2, d3] = this.dates;
+ // prettier-ignore
+ return [
+ [
+ [ "00:30", "00:45", Date.parse(`${d1}T00:30:00.000-07:00`), Date.parse(`${d1}T00:45:00.000-07:00`) ],
+ [ "00:30", "00:45", Date.parse(`${d2}T00:30:00.000-07:00`), Date.parse(`${d2}T00:45:00.000-07:00`) ],
+ [ "00:30", "00:45", Date.parse(`${d3}T00:30:00.000-08:00`), Date.parse(`${d3}T00:45:00.000-08:00`) ],
+ ],
+ [
+ [ "00:30", "01:00", Date.parse(`${d1}T00:30:00.000-07:00`), Date.parse(`${d1}T01:00:00.000-07:00`) ],
+ [ "00:30", "01:00", Date.parse(`${d2}T00:30:00.000-07:00`), Date.parse(`${d2}T01:00:00.000-07:00`) ],
+ [ "00:30", "01:00", Date.parse(`${d3}T00:30:00.000-08:00`), Date.parse(`${d3}T01:00:00.000-08:00`) ],
+ ],
+ [
+ [ "00:30", "01:30", Date.parse(`${d1}T00:30:00.000-07:00`), Date.parse(`${d1}T01:30:00.000-07:00`) ],
+ [ "00:30", "01:30", Date.parse(`${d2}T00:30:00.000-07:00`), Date.parse(`${d2}T01:30:00.000-07:00`) ],
+ [ "00:30", "01:30", Date.parse(`${d3}T00:30:00.000-08:00`), Date.parse(`${d3}T01:30:00.000-08:00`) ],
+ ],
+ [
+ [ "00:30", "02:00", Date.parse(`${d1}T00:30:00.000-07:00`), Date.parse(`${d1}T02:00:00.000-07:00`) ],
+ [ "00:30", "02:00", Date.parse(`${d2}T00:30:00.000-07:00`), Date.parse(`${d2}T01:00:00.000-08:00`) ],
+ [ "00:30", "02:00", Date.parse(`${d3}T00:30:00.000-08:00`), Date.parse(`${d3}T02:00:00.000-08:00`) ],
+ ],
+ [
+ [ "00:30", "02:30", Date.parse(`${d1}T00:30:00.000-07:00`), Date.parse(`${d1}T02:30:00.000-07:00`) ],
+ [ "00:30", "02:30", Date.parse(`${d2}T00:30:00.000-07:00`), Date.parse(`${d2}T01:30:00.000-08:00`) ],
+ [ "00:30", "02:30", Date.parse(`${d3}T00:30:00.000-08:00`), Date.parse(`${d3}T02:30:00.000-08:00`) ],
+ ],
+ [
+ [ "01:00", "01:30", Date.parse(`${d1}T01:00:00.000-07:00`), Date.parse(`${d1}T01:30:00.000-07:00`) ],
+ [ "01:00", "01:30", Date.parse(`${d2}T01:00:00.000-07:00`), Date.parse(`${d2}T01:30:00.000-07:00`) ],
+ [ "01:00", "01:30", Date.parse(`${d3}T01:00:00.000-08:00`), Date.parse(`${d3}T01:30:00.000-08:00`) ],
+ ],
+ [
+ [ "01:00", "02:00", Date.parse(`${d1}T01:00:00.000-07:00`), Date.parse(`${d1}T02:00:00.000-07:00`) ],
+ [ "01:00", "02:00", Date.parse(`${d2}T01:00:00.000-07:00`), Date.parse(`${d2}T01:00:00.000-08:00`) ],
+ [ "01:00", "02:00", Date.parse(`${d3}T01:00:00.000-08:00`), Date.parse(`${d3}T02:00:00.000-08:00`) ],
+ ],
+ [
+ [ "01:00", "02:30", Date.parse(`${d1}T01:00:00.000-07:00`), Date.parse(`${d1}T02:30:00.000-07:00`) ],
+ [ "01:00", "02:30", Date.parse(`${d2}T01:00:00.000-07:00`), Date.parse(`${d2}T01:30:00.000-08:00`) ],
+ [ "01:00", "02:30", Date.parse(`${d3}T01:00:00.000-08:00`), Date.parse(`${d3}T02:30:00.000-08:00`) ],
+ ],
+ [
+ [ "01:30", "01:45", Date.parse(`${d1}T01:30:00.000-07:00`), Date.parse(`${d1}T01:45:00.000-07:00`) ],
+ [ "01:30", "01:45", Date.parse(`${d2}T01:30:00.000-07:00`), Date.parse(`${d2}T01:45:00.000-07:00`) ],
+ [ "01:30", "01:45", Date.parse(`${d3}T01:30:00.000-08:00`), Date.parse(`${d3}T01:45:00.000-08:00`) ],
+ ],
+ [
+ [ "01:30", "02:00", Date.parse(`${d1}T01:30:00.000-07:00`), Date.parse(`${d1}T02:00:00.000-07:00`) ],
+ [ "01:30", "02:00", Date.parse(`${d2}T01:30:00.000-07:00`), Date.parse(`${d2}T01:00:00.000-08:00`) ],
+ [ "01:30", "02:00", Date.parse(`${d3}T01:30:00.000-08:00`), Date.parse(`${d3}T02:00:00.000-08:00`) ],
+ ],
+ [
+ [ "01:30", "02:30", Date.parse(`${d1}T01:30:00.000-07:00`), Date.parse(`${d1}T02:30:00.000-07:00`) ],
+ [ "01:30", "02:30", Date.parse(`${d2}T01:30:00.000-07:00`), Date.parse(`${d2}T01:30:00.000-08:00`) ],
+ [ "01:30", "02:30", Date.parse(`${d3}T01:30:00.000-08:00`), Date.parse(`${d3}T02:30:00.000-08:00`) ],
+ ],
+ [
+ [ "02:00", "02:30", Date.parse(`${d1}T02:00:00.000-07:00`), Date.parse(`${d1}T02:30:00.000-07:00`) ],
+ [ "02:00", "02:30", Date.parse(`${d2}T02:00:00.000-08:00`), Date.parse(`${d2}T02:30:00.000-08:00`) ],
+ [ "02:00", "02:30", Date.parse(`${d3}T02:00:00.000-08:00`), Date.parse(`${d3}T02:30:00.000-08:00`) ],
+ ]
+ ];
+ },
+};
+
+module.exports = {
+ generateTestCases,
+ WETnonrecurring,
+ WESTnonrecurring,
+ WETtoWESTnonrecurring,
+ WETtoWESTrecurring,
+ WESTtoWETnonrecurring,
+ WESTtoWETrecurring,
+ PSTnonrecurring,
+ PDTnonrecurring,
+ PSTtoPDTnonrecurring,
+ PSTtoPDTrecurring,
+ PDTtoPSTnonrecurring,
+ PDTtoPSTrecurring,
+};
diff --git a/server/test/unit/httpError.test.js b/server/test/unit/httpError.test.js
new file mode 100644
index 00000000..21aeda80
--- /dev/null
+++ b/server/test/unit/httpError.test.js
@@ -0,0 +1,17 @@
+const httpError = require("../../utilities/httpError");
+
+describe("httpError", () => {
+ it('should return a custom error with message: "test"', () => {
+ const res = httpError(400, "test");
+ expect(res).toBeInstanceOf(Error);
+ expect(res.status).toBe(400);
+ expect(res.message).toBe("test");
+ });
+
+ it("should return a custom error with default message", () => {
+ const res = httpError(401);
+ expect(res).toBeInstanceOf(Error);
+ expect(res.status).toBe(401);
+ expect(res.message).toBe("Unauthorized");
+ });
+});
diff --git a/server/test/unit/validateBody.test.js b/server/test/unit/validateBody.test.js
new file mode 100644
index 00000000..58f60413
--- /dev/null
+++ b/server/test/unit/validateBody.test.js
@@ -0,0 +1,155 @@
+"use strict";
+
+const validateBody = require("../../middleware/validateBody");
+const {
+ createEventSchema,
+ MAX_RECURRENCE_PERIOD,
+ EVENT_MAX_DATE,
+} = require("../../models/Event");
+const sample = require("./validateBodyMockData");
+
+const mockRequest = body => ({ body });
+const mockNext = () => jest.fn();
+
+describe("validateBody", () => {
+ describe("req.body object has no errors", () => {
+ it("should call next for valid nonrecurring event", () => {
+ const req = mockRequest(sample.validFormDataNonRecurr);
+ const next = mockNext();
+ validateBody(createEventSchema)(req, null, next);
+ expect(next).toBeCalled();
+ });
+
+ it("should call next for valid recurring event", () => {
+ const req = mockRequest(sample.validFormDataRecurr);
+ const next = mockNext();
+ validateBody(createEventSchema)(req, null, next);
+ expect(next).toBeCalled();
+ });
+ });
+
+ describe("req.body object shape", () => {
+ it("should error when a field is missing", () => {
+ const req = mockRequest(sample.missingTitle);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ '"title" is required'
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it("should error when there's an alien field", () => {
+ const req = mockRequest(sample.alienProp);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /is not allowed/
+ );
+ expect(next).not.toBeCalled();
+ });
+ });
+
+ describe("string fields", () => {
+ it("should error when title is empty", () => {
+ const req = mockRequest(sample.emptyTitle);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ '"title" is not allowed to be empty'
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it("should error when title is too long", () => {
+ const req = mockRequest(sample.titleToLong);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /length must be less than or equal to/
+ );
+ expect(next).not.toBeCalled();
+ });
+ });
+
+ describe("date fields", () => {
+ it('should error when "initialDate" is in wrong format', () => {
+ const req = mockRequest(sample.initialDateWrongFormat);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /fails to match the required pattern/
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it("should error when start date is in the past", () => {
+ const req = mockRequest(sample.startDateInThePast);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /Event should start in the future/
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it("should error when 'finalDate' is not equal to 'initialDate' for nonrecurring events", () => {
+ const req = mockRequest(sample.finalDateGreaterThanStartDateNonrecurr);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /finalDate should be equal to initialDate for non-recurring events/
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it("should error when 'finalDate' is before 'initialDate' for recurring events", () => {
+ const req = mockRequest(sample.finalDateLessThanStartDateRecurr);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /finalDate must be greater than or equal to initialDate for recurring events/
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it(`should error when 'finalDate' is more than ${MAX_RECURRENCE_PERIOD} days from 'initialDate' for recurring events`, () => {
+ const req = mockRequest(sample.exceedMaxPeriod);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /must be within/
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it(`should error when recurring event starts before and ends after ${EVENT_MAX_DATE}`, () => {
+ const req = mockRequest(sample.startBeforeEndAfterMAX);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /finalDate must be before/
+ );
+ expect(next).not.toBeCalled();
+ });
+ });
+
+ describe("'recurring' field", () => {
+ it('should error when rucurring rate is not "noRecurr" or "weekly"', () => {
+ const req = mockRequest(sample.invalidRate);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /must be one of/
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it('should error when rate if "noRecurr" and days is not empty', () => {
+ const req = mockRequest(sample.invalidNonRecurDays);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /must contain 0 items/
+ );
+ expect(next).not.toBeCalled();
+ });
+
+ it("should error when day of the week is invalid", () => {
+ const req = mockRequest(sample.invalidDayName);
+ const next = mockNext();
+ expect(() => validateBody(createEventSchema)(req, null, next)).toThrow(
+ /must be one of/
+ );
+ expect(next).not.toBeCalled();
+ });
+ });
+});
diff --git a/server/test/unit/validateBodyMockData.js b/server/test/unit/validateBodyMockData.js
new file mode 100644
index 00000000..89ad249b
--- /dev/null
+++ b/server/test/unit/validateBodyMockData.js
@@ -0,0 +1,138 @@
+"use strict";
+
+const { Temporal } = require("@js-temporal/polyfill");
+
+const {
+ STRING_MAX_LENGTH,
+ MAX_RECURRENCE_PERIOD,
+ EVENT_MAX_DATE,
+} = require("../../models/Event");
+
+const dateNow = Temporal.Now.plainDateISO();
+const dateYesterday = dateNow.subtract({ days: 1 });
+const dateTomorrow = dateNow.add({ days: 1 });
+const dateIn5Days = dateNow.add({ days: 5 });
+const dateAfterMaxPeriod = dateNow.add({ days: MAX_RECURRENCE_PERIOD + 1 });
+const dateBeforeMax = Temporal.PlainDate.from(EVENT_MAX_DATE).subtract({
+ days: 2,
+});
+const dateAfterMax = Temporal.PlainDate.from(EVENT_MAX_DATE).add({
+ days: 2,
+});
+
+const timeNow = Temporal.Now.plainTimeISO().round({
+ smallestUnit: "minute",
+ roundingMode: "ceil",
+});
+const timeIn1hour = timeNow.subtract({ hours: 1 });
+
+const validFormDataNonRecurr = {
+ title: "test",
+ description: "test",
+ location: "test",
+ discordName: "test",
+ recurring: {
+ rate: "noRecurr",
+ days: [],
+ },
+ initialDate: dateNow.toString(),
+ startTime: timeNow.toString().slice(0, 5),
+ finalDate: dateNow.toString(),
+ endTime: timeIn1hour.toString().slice(0, 5),
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+};
+
+const validFormDataRecurr = {
+ title: "test",
+ description: "test",
+ location: "test",
+ discordName: "test",
+ recurring: {
+ rate: "weekly",
+ days: ["1", "2", "3", "4", "5", "6", "7"],
+ },
+ initialDate: dateNow.toString(),
+ startTime: timeNow.toString().slice(0, 5),
+ finalDate: dateIn5Days.toString(),
+ endTime: timeIn1hour.toString().slice(0, 5),
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+};
+
+const missingTitle = {
+ ...validFormDataNonRecurr,
+};
+delete missingTitle["title"];
+
+const emptyTitle = { ...validFormDataNonRecurr, title: "" };
+
+const titleToLong = {
+ ...validFormDataNonRecurr,
+ title: "f".repeat(STRING_MAX_LENGTH + 1),
+};
+
+const initialDateWrongFormat = {
+ ...validFormDataNonRecurr,
+ initialDate: "2023/11/30",
+};
+
+const startDateInThePast = {
+ ...validFormDataRecurr,
+ initialDate: dateYesterday.toString(),
+};
+
+const finalDateLessThanStartDateRecurr = {
+ ...validFormDataRecurr,
+ initialDate: dateIn5Days.toString(),
+ finalDate: dateTomorrow.toString(),
+};
+
+const finalDateGreaterThanStartDateNonrecurr = {
+ ...validFormDataNonRecurr,
+ finalDate: dateTomorrow.toString(),
+};
+
+const exceedMaxPeriod = {
+ ...validFormDataRecurr,
+ finalDate: dateAfterMaxPeriod.toString(),
+};
+
+const startBeforeEndAfterMAX = {
+ ...validFormDataRecurr,
+ initialDate: dateBeforeMax.toString(),
+ finalDate: dateAfterMax.toString(),
+};
+
+const alienProp = { ...validFormDataNonRecurr, a: 1 };
+
+const invalidRate = {
+ ...validFormDataNonRecurr,
+ recurring: { rate: "invalid value", days: [] },
+};
+
+const invalidNonRecurDays = {
+ ...validFormDataNonRecurr,
+ recurring: { rate: "noRecurr", days: ["Monday"] },
+};
+
+const invalidDayName = {
+ ...validFormDataRecurr,
+ recurring: { rate: "weekly", days: ["January!"] },
+};
+
+module.exports = {
+ validFormDataNonRecurr,
+ validFormDataRecurr,
+ missingTitle,
+ emptyTitle,
+ titleToLong,
+ initialDateWrongFormat,
+ startDateInThePast,
+ finalDateGreaterThanStartDateNonrecurr,
+ exceedMaxPeriod,
+ finalDateLessThanStartDateRecurr,
+ startBeforeEndAfterMAX,
+ alienProp,
+ invalidRate,
+ invalidNonRecurDays,
+ invalidDayName,
+};
diff --git a/server/test/unit/validateObjectId.test.js b/server/test/unit/validateObjectId.test.js
new file mode 100644
index 00000000..64780560
--- /dev/null
+++ b/server/test/unit/validateObjectId.test.js
@@ -0,0 +1,26 @@
+const validateObjectId = require("../../middleware/validateObjectId");
+
+// Request is an object that contains 'params' object
+const mockRequest = params => ({ params });
+// const mockResponse = () => {
+// const res = {};
+// res.status = jest.fn().mockReturnValue(res);
+// return res;
+// };
+const mockNext = () => jest.fn();
+
+describe("validateObjectId", () => {
+ it("should throw an error if mongoose ObjectId is invalid", () => {
+ const req = mockRequest({ id: "1" });
+ const next = mockNext();
+ expect(() => validateObjectId(req, null, next)).toThrow("Not found");
+ expect(next).not.toHaveBeenCalled();
+ });
+
+ it("should call next() if mongoose ObjectId is valid", () => {
+ const req = mockRequest({ id: "63c722239b1a9104e164d728" });
+ const next = mockNext();
+ expect(() => validateObjectId(req, null, next)).not.toThrow("Not found");
+ expect(next).toHaveBeenCalled();
+ });
+});
diff --git a/server/test/utils.js b/server/test/utils.js
new file mode 100644
index 00000000..ee571798
--- /dev/null
+++ b/server/test/utils.js
@@ -0,0 +1,35 @@
+"use strict";
+
+const mongoose = require("mongoose");
+const { MongoMemoryServer } = require("mongodb-memory-server");
+
+class Database {
+ async setUp() {
+ try {
+ this.mongod = await MongoMemoryServer.create();
+ const url = this.mongod.getUri();
+ await mongoose.connect(url, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true,
+ useFindAndModify: false,
+ useCreateIndex: true,
+ });
+ } catch (err) {
+ console.error(err);
+ process.exit(1);
+ }
+ }
+
+ async tearDown() {
+ try {
+ await mongoose.connection.dropDatabase();
+ await mongoose.disconnect();
+ await this.mongod.stop();
+ } catch (err) {
+ console.error(err);
+ process.exit(1);
+ }
+ }
+}
+
+module.exports = { Database };
diff --git a/server/utilities/createEventsArray.js b/server/utilities/createEventsArray.js
index 4c37c949..646d221c 100644
--- a/server/utilities/createEventsArray.js
+++ b/server/utilities/createEventsArray.js
@@ -1,6 +1,19 @@
-const eachDayOfInterval = require("date-fns/eachDayOfInterval");
-const format = require("date-fns/format");
+"use strict";
+
const { nanoid } = require("nanoid");
+const { Temporal } = require("@js-temporal/polyfill");
+
+/**
+ * @param {string} date 'yyyy-mm-dd'
+ * @param {string} time 'hh:mm'
+ * @returns {Object} { year, month, day, hour, minute }
+ */
+function parseHtmlDatetime(date, time) {
+ // 'yyyy-mm-dd' and 'hh:mm'
+ const [year, month, day] = date.split("-");
+ const [hour, minute] = time.split(":");
+ return { year, month, day, hour, minute };
+}
/**
* Generates an array of events
@@ -12,42 +25,68 @@ const createEventsArray = ({
title,
description,
location,
- firstEventStart,
- firstEventEnd,
- lastEventStart,
+ initialDate,
+ finalDate,
+ startTime,
+ endTime,
+ timeZone,
}) => {
const { rate, days } = recurring;
+ // Time on the clock when the first event starts
+ const plainFirstEventStart = Temporal.PlainDateTime.from(
+ parseHtmlDatetime(initialDate, startTime)
+ );
+
+ // Time on the clock when the first event ends
+ const plainFirstEventEnd = Temporal.PlainDateTime.from(
+ parseHtmlDatetime(initialDate, endTime)
+ );
+
+ // Time on the clock when the last event starts
+ const plainLastEventStart = Temporal.PlainDateTime.from(
+ parseHtmlDatetime(finalDate, startTime)
+ );
+
+ // Duration is calculated based on the time clock independent of the timezone
+ // e.g. event on March 12, 1AM-2AM implies that the duration is 1 hour
+ const duration =
+ plainFirstEventStart.until(plainFirstEventEnd).sign === 1
+ ? plainFirstEventStart.until(plainFirstEventEnd)
+ : plainFirstEventStart.until(plainFirstEventEnd.add({ days: 1 }));
+
+ // Add timezone to the plain clock times
+ const zonedFirstEventStart = plainFirstEventStart.toZonedDateTime(timeZone);
+ const zonedLastEventStart = plainLastEventStart.toZonedDateTime(timeZone);
+
// Array of start times
const eventStartDates = [];
- let iter = new Date(firstEventStart);
- while (iter <= lastEventStart) {
- const utcDay = iter.getUTCDay().toString();
- // push to array if the recurring day in in the list, or if event is non-recurring
- if (days.includes(utcDay) || rate === "noRecurr") {
- eventStartDates.push(new Date(iter));
+
+ let i = 0;
+ while (
+ Temporal.ZonedDateTime.compare(
+ zonedFirstEventStart.add({ days: i }),
+ zonedLastEventStart
+ ) <= 0
+ ) {
+ if (
+ days.includes(
+ zonedFirstEventStart.add({ days: i }).dayOfWeek.toString()
+ ) ||
+ rate === "noRecurr"
+ ) {
+ eventStartDates.push(zonedFirstEventStart.add({ days: i }));
}
- iter.setDate(iter.getDate() + 1);
+ i += 1;
}
// Recurring events have the same group id. This allows deleting them all at once by this id.
const groupId = rate === "noRecurr" ? null : nanoid();
// Create dates array with events information
- const events = eventStartDates.map(startAt => {
- let endAt = new Date(firstEventEnd);
- // The order of setting date, month, and year is important!
- endAt.setDate(startAt.getDate());
- endAt.setMonth(startAt.getMonth());
- endAt.setFullYear(startAt.getFullYear());
-
- if (startAt > endAt) {
- endAt.setDate(endAt.getDate() + 1);
- }
-
- startAt = startAt.getTime();
- endAt = endAt.getTime();
-
+ const events = eventStartDates.map(date => {
+ const startAt = date.epochMilliseconds;
+ const endAt = date.add(duration).epochMilliseconds;
return { title, description, location, groupId, startAt, endAt, rsvp: [] };
});
return events;