Skip to content

Commit

Permalink
feat: user schedule management (#13053)
Browse files Browse the repository at this point in the history
* platform-constants package: list of accepted schedule timezones

* feat: schedules endpoint to create schedule with default availability

* refactor: rename function

* refactor: store userId for availabilities

* feat: createSchedule endpoint

* feat: get schedules/default

* feat: getSchedule by id

* feat: get all schedules

* feat: delete schedule

* feat: update schedule

* check user owns schedule

* empty test

* define returned data on controller level not repository

* define returned data on controller level not repository

* Revert "define returned data on controller level not repository"

This reverts commit 4c292a0.

* use luxton

* put availabilities out of ee

* use guard on controller level

* refactor

* e2e test schedule creation

* remove log

* test

* default schedule get test

* update schedule test

* delete schedule test

* fix update test

* different email for schedules e2e

* driveby: fix yarn test

* schedule inputs availabilities as array

* re-use BaseStrategy class
  • Loading branch information
supalarry committed Jan 16, 2024
1 parent 66740a2 commit e5d4554
Show file tree
Hide file tree
Showing 25 changed files with 1,247 additions and 23 deletions.
3 changes: 2 additions & 1 deletion apps/api/v2/src/ee/platform-endpoints-module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { GcalModule } from "@/ee/gcal/gcal.module";
import { ProviderModule } from "@/ee/provider/provider.module";
import { SchedulesModule } from "@/ee/schedules/schedules.module";
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
import { Module } from "@nestjs/common";

@Module({
imports: [GcalModule, ProviderModule],
imports: [GcalModule, ProviderModule, SchedulesModule],
})
export class PlatformEndpointsModule implements NestModule {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
import { ScheduleResponse } from "@/ee/schedules/zod/response/response";
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersModule } from "@/modules/users/users.module";
import { INestApplication } from "@nestjs/common";
import { NestExpressApplication } from "@nestjs/platform-express";
import { Test } from "@nestjs/testing";
import { User } from "@prisma/client";
import * as request from "supertest";
import { SchedulesRepositoryFixture } from "test/fixtures/repository/schedules.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";

import { SUCCESS_STATUS } from "@calcom/platform-constants";
import { ApiSuccessResponse } from "@calcom/platform-types";

describe("Schedules Endpoints", () => {
describe("User Authentication", () => {
let app: INestApplication;

let userRepositoryFixture: UserRepositoryFixture;
let scheduleRepositoryFixture: SchedulesRepositoryFixture;

const userEmail = "schedules-controller-e2e@api.com";
let user: User;

let createdSchedule: ScheduleResponse;

beforeAll(async () => {
const moduleRef = await withAccessTokenAuth(
userEmail,
Test.createTestingModule({
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule],
providers: [SchedulesRepository, SchedulesService],
})
).compile();

userRepositoryFixture = new UserRepositoryFixture(moduleRef);
scheduleRepositoryFixture = new SchedulesRepositoryFixture(moduleRef);
user = await userRepositoryFixture.create({
email: userEmail,
});

app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);

await app.init();
});

it("should be defined", () => {
expect(userRepositoryFixture).toBeDefined();
expect(user).toBeDefined();
});

it("should create a default schedule", async () => {
const scheduleName = "schedule-name";
const scheduleTimeZone = "Europe/Rome";

const body = {
name: scheduleName,
timeZone: scheduleTimeZone,
};

return request(app.getHttpServer())
.post("/api/v2/schedules")
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<{ schedule: ScheduleResponse }> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();

expect(responseBody.data.schedule).toBeDefined();
expect(responseBody.data.schedule.id).toBeDefined();
expect(responseBody.data.schedule.userId).toEqual(user.id);
expect(responseBody.data.schedule.name).toEqual(scheduleName);
expect(responseBody.data.schedule.timeZone).toEqual(scheduleTimeZone);

expect(responseBody.data.schedule.availability).toBeDefined();
expect(responseBody.data.schedule.availability?.length).toEqual(1);
const defaultAvailabilityDays = [1, 2, 3, 4, 5];
const defaultAvailabilityStartTime = "09:00:00";
const defaultAvailabilityEndTime = "17:00:00";

expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(defaultAvailabilityDays);
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(
defaultAvailabilityStartTime
);
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(defaultAvailabilityEndTime);

const scheduleUser = await userRepositoryFixture.get(responseBody.data.schedule.userId);
expect(scheduleUser?.defaultScheduleId).toEqual(responseBody.data.schedule.id);
await scheduleRepositoryFixture.deleteById(responseBody.data.schedule.id);
await scheduleRepositoryFixture.deleteAvailabilities(responseBody.data.schedule.id);
});
});

it("should create a schedule", async () => {
const scheduleName = "schedule-name";
const scheduleTimeZone = "Europe/Rome";
const availabilityDays = [1, 2, 3, 4, 5, 6];
const availabilityStartTime = "11:00:00";
const availabilityEndTime = "14:00:00";

const body = {
name: scheduleName,
timeZone: scheduleTimeZone,
availabilities: [
{
days: availabilityDays,
startTime: availabilityStartTime,
endTime: availabilityEndTime,
},
],
};

return request(app.getHttpServer())
.post("/api/v2/schedules")
.send(body)
.expect(201)
.then(async (response) => {
const responseBody: ApiSuccessResponse<{ schedule: ScheduleResponse }> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();

expect(responseBody.data.schedule).toBeDefined();
expect(responseBody.data.schedule.id).toBeDefined();
expect(responseBody.data.schedule.userId).toEqual(user.id);
expect(responseBody.data.schedule.name).toEqual(scheduleName);
expect(responseBody.data.schedule.timeZone).toEqual(scheduleTimeZone);

expect(responseBody.data.schedule.availability).toBeDefined();
expect(responseBody.data.schedule.availability?.length).toEqual(1);
expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(availabilityDays);
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(availabilityStartTime);
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(availabilityEndTime);

createdSchedule = responseBody.data.schedule;

const scheduleUser = await userRepositoryFixture.get(responseBody.data.schedule.userId);
expect(scheduleUser?.defaultScheduleId).toEqual(responseBody.data.schedule.id);
});
});

it("should get default schedule", async () => {
return request(app.getHttpServer())
.get("/api/v2/schedules/default")
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<{ schedule: ScheduleResponse }> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();

expect(responseBody.data.schedule).toBeDefined();
expect(responseBody.data.schedule.id).toBeDefined();
expect(responseBody.data.schedule.userId).toEqual(createdSchedule.userId);
expect(responseBody.data.schedule.name).toEqual(createdSchedule.name);
expect(responseBody.data.schedule.timeZone).toEqual(createdSchedule.timeZone);

expect(responseBody.data.schedule.availability).toBeDefined();
expect(responseBody.data.schedule.availability?.length).toEqual(1);

expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(
createdSchedule.availability?.[0]?.days
);
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(
createdSchedule.availability?.[0]?.startTime
);
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(
createdSchedule.availability?.[0]?.endTime
);
});
});

it("should get schedule", async () => {
return request(app.getHttpServer())
.get(`/api/v2/schedules/${createdSchedule.id}`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<{ schedule: ScheduleResponse }> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();

expect(responseBody.data.schedule).toBeDefined();
expect(responseBody.data.schedule.id).toBeDefined();
expect(responseBody.data.schedule.userId).toEqual(createdSchedule.userId);
expect(responseBody.data.schedule.name).toEqual(createdSchedule.name);
expect(responseBody.data.schedule.timeZone).toEqual(createdSchedule.timeZone);

expect(responseBody.data.schedule.availability).toBeDefined();
expect(responseBody.data.schedule.availability?.length).toEqual(1);

expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(
createdSchedule.availability?.[0]?.days
);
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(
createdSchedule.availability?.[0]?.startTime
);
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(
createdSchedule.availability?.[0]?.endTime
);
});
});

it("should get schedules", async () => {
return request(app.getHttpServer())
.get(`/api/v2/schedules`)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<{ schedules: ScheduleResponse[] }> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();

expect(responseBody.data.schedules).toBeDefined();
expect(responseBody.data.schedules.length).toEqual(1);

const fetchedSchedule = responseBody.data.schedules[0];
expect(fetchedSchedule).toBeDefined();
expect(fetchedSchedule.userId).toEqual(createdSchedule.userId);
expect(fetchedSchedule.name).toEqual(createdSchedule.name);
expect(fetchedSchedule.timeZone).toEqual(createdSchedule.timeZone);

expect(fetchedSchedule.availability).toBeDefined();
expect(fetchedSchedule.availability?.length).toEqual(1);

expect(fetchedSchedule.availability?.[0]?.days).toEqual(createdSchedule.availability?.[0]?.days);
expect(fetchedSchedule.availability?.[0]?.startTime).toEqual(
createdSchedule.availability?.[0]?.startTime
);
expect(fetchedSchedule.availability?.[0]?.endTime).toEqual(
createdSchedule.availability?.[0]?.endTime
);
});
});

it("should update schedule name", async () => {
const newScheduleName = "new-schedule-name";

const body = {
name: newScheduleName,
};

return request(app.getHttpServer())
.put(`/api/v2/schedules/${createdSchedule.id}`)
.send(body)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<{ schedule: ScheduleResponse }> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();

expect(responseBody.data.schedule).toBeDefined();
expect(responseBody.data.schedule.id).toBeDefined();
expect(responseBody.data.schedule.userId).toEqual(createdSchedule.userId);
expect(responseBody.data.schedule.name).toEqual(newScheduleName);
expect(responseBody.data.schedule.timeZone).toEqual(createdSchedule.timeZone);

expect(responseBody.data.schedule.availability).toBeDefined();
expect(responseBody.data.schedule.availability?.length).toEqual(1);

expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(
createdSchedule.availability?.[0]?.days
);
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(
createdSchedule.availability?.[0]?.startTime
);
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(
createdSchedule.availability?.[0]?.endTime
);

createdSchedule = responseBody.data.schedule;
});
});

it("should update schedule availabilities", async () => {
const newAvailabilityDays = [2, 4];
const newAvailabilityStartTime = "19:00:00";
const newAvailabilityEndTime = "20:00:00";

const body = {
availabilities: [
{
days: newAvailabilityDays,
startTime: newAvailabilityStartTime,
endTime: newAvailabilityEndTime,
},
],
};

return request(app.getHttpServer())
.put(`/api/v2/schedules/${createdSchedule.id}`)
.send(body)
.expect(200)
.then((response) => {
const responseBody: ApiSuccessResponse<{ schedule: ScheduleResponse }> = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();

expect(responseBody.data.schedule).toBeDefined();
expect(responseBody.data.schedule.id).toBeDefined();
expect(responseBody.data.schedule.userId).toEqual(createdSchedule.userId);
expect(responseBody.data.schedule.name).toEqual(createdSchedule.name);
expect(responseBody.data.schedule.timeZone).toEqual(createdSchedule.timeZone);

expect(responseBody.data.schedule.availability).toBeDefined();
expect(responseBody.data.schedule.availability?.length).toEqual(1);

expect(responseBody.data.schedule.availability?.[0]?.days).toEqual(newAvailabilityDays);
expect(responseBody.data.schedule.availability?.[0]?.startTime).toEqual(newAvailabilityStartTime);
expect(responseBody.data.schedule.availability?.[0]?.endTime).toEqual(newAvailabilityEndTime);

createdSchedule = responseBody.data.schedule;
});
});

it("should delete schedule", async () => {
return request(app.getHttpServer()).delete(`/api/v2/schedules/${createdSchedule.id}`).expect(200);
});

afterAll(async () => {
await userRepositoryFixture.deleteByEmail(user.email);
await app.close();
});
});
});
Loading

0 comments on commit e5d4554

Please sign in to comment.