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

feat: user schedule management #13053

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bd593a5
platform-constants package: list of accepted schedule timezones
supalarry Jan 5, 2024
adacba6
feat: schedules endpoint to create schedule with default availability
supalarry Jan 5, 2024
0ab39dd
refactor: rename function
supalarry Jan 5, 2024
e980786
refactor: store userId for availabilities
supalarry Jan 5, 2024
4025453
feat: createSchedule endpoint
supalarry Jan 8, 2024
65c4b7c
feat: get schedules/default
supalarry Jan 8, 2024
f6b6f9c
feat: getSchedule by id
supalarry Jan 8, 2024
e464e02
feat: get all schedules
supalarry Jan 8, 2024
6b8b8ab
feat: delete schedule
supalarry Jan 8, 2024
f87e88a
feat: update schedule
supalarry Jan 8, 2024
1e5cc77
check user owns schedule
supalarry Jan 8, 2024
82a7b24
empty test
supalarry Jan 8, 2024
7223de0
Merge branch 'platform' into lauris/pro-161-crud-endpoints-for-user-s…
supalarry Jan 8, 2024
a923934
define returned data on controller level not repository
supalarry Jan 8, 2024
4c292a0
define returned data on controller level not repository
supalarry Jan 8, 2024
3dfcb2c
Revert "define returned data on controller level not repository"
supalarry Jan 8, 2024
afca5a6
use luxton
supalarry Jan 8, 2024
736da72
put availabilities out of ee
supalarry Jan 8, 2024
d8a511b
use guard on controller level
supalarry Jan 8, 2024
266b36d
refactor
supalarry Jan 9, 2024
0e6f969
e2e test schedule creation
supalarry Jan 9, 2024
f35cb43
remove log
supalarry Jan 9, 2024
7c4cfdf
test
supalarry Jan 9, 2024
118e9e8
default schedule get test
supalarry Jan 9, 2024
07697a7
update schedule test
supalarry Jan 9, 2024
04eea39
delete schedule test
supalarry Jan 9, 2024
8fcec9f
fix update test
supalarry Jan 9, 2024
3c82acf
different email for schedules e2e
supalarry Jan 9, 2024
0f01e60
driveby: fix yarn test
supalarry Jan 10, 2024
3ed296e
schedule inputs availabilities as array
supalarry Jan 15, 2024
5be00d6
re-use BaseStrategy class
supalarry Jan 15, 2024
ca39458
Merge branch 'platform' into lauris/pro-161-crud-endpoints-for-user-s…
supalarry Jan 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading