Skip to content

Commit

Permalink
Merge branch 'main' into refactor_allow_managed_event_tests_parallel_…
Browse files Browse the repository at this point in the history
…runs
  • Loading branch information
zomars authored Aug 15, 2024
2 parents 55b0b90 + 08e1b0a commit e7c2c8b
Show file tree
Hide file tree
Showing 70 changed files with 3,019 additions and 323 deletions.
5 changes: 4 additions & 1 deletion .github/workflows/pr-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ on:
jobs:
label-pr:
runs-on: ubuntu-latest

permissions:
actions: read
issues: write
pull-requests: write
steps:
- name: Label PR as ready for E2E
if: github.event.review.state == 'approved'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ class Primary {
credentialId!: number;
}

class Calendar {
export class Calendar {
@IsEmail()
externalId!: string;

Expand Down Expand Up @@ -135,7 +135,7 @@ class Calendar {
credentialId!: number;
}

class ConnectedCalendar {
export class ConnectedCalendar {
@ValidateNested()
@IsObject()
integration!: Integration;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { bootstrap } from "@/app";
import { AppModule } from "@/app.module";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { HttpExceptionFilter } from "@/filters/http-exception.filter";
import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { DestinationCalendarsOutputResponseDto } from "@/modules/destination-calendars/outputs/destination-calendars.output";
import { TokensModule } from "@/modules/tokens/tokens.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 { PlatformOAuthClient, Team, User, Credential } from "@prisma/client";
import * as request from "supertest";
import { CredentialsRepositoryFixture } from "test/fixtures/repository/credentials.repository.fixture";
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture";
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";

import { APPLE_CALENDAR_TYPE, APPLE_CALENDAR_ID } from "@calcom/platform-constants";
import { SUCCESS_STATUS } from "@calcom/platform-constants";

const CLIENT_REDIRECT_URI = "http://localhost:5555";

describe("Platform Destination Calendar Endpoints", () => {
let app: INestApplication;

let oAuthClient: PlatformOAuthClient;
let organization: Team;
let userRepositoryFixture: UserRepositoryFixture;
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
let teamRepositoryFixture: TeamRepositoryFixture;
let tokensRepositoryFixture: TokensRepositoryFixture;
let credentialsRepositoryFixture: CredentialsRepositoryFixture;
let appleCalendarCredentials: Credential;
let user: User;
let accessTokenSecret: string;
let refreshTokenSecret: string;

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
providers: [PrismaExceptionFilter, HttpExceptionFilter],
imports: [AppModule, UsersModule, TokensModule],
})
.overrideGuard(PermissionsGuard)
.useValue({
canActivate: () => true,
})

.compile();

oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
credentialsRepositoryFixture = new CredentialsRepositoryFixture(moduleRef);
organization = await teamRepositoryFixture.create({ name: "organization" });
oAuthClient = await createOAuthClient(organization.id);
user = await userRepositoryFixture.createOAuthManagedUser("office365-connect@gmail.com", oAuthClient.id);
const tokens = await tokensRepositoryFixture.createTokens(user.id, oAuthClient.id);
accessTokenSecret = tokens.accessToken;
refreshTokenSecret = tokens.refreshToken;
appleCalendarCredentials = await credentialsRepositoryFixture.create(
APPLE_CALENDAR_TYPE,
{},
user.id,
APPLE_CALENDAR_ID
);
jest.spyOn(CalendarsService.prototype, "getCalendars").mockReturnValue(
Promise.resolve({
connectedCalendars: [
{
integration: {
installed: false,
type: "apple_calendar",
title: "",
name: "",
description: "",
variant: "calendar",
slug: "",
locationOption: null,
categories: ["calendar"],
logo: "",
publisher: "",
url: "",
email: "",
},
calendars: {
externalId:
"https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
readOnly: false,
integration: "apple_calendar",
credentialId: appleCalendarCredentials.id,
primary: true,
email: user.email,
},
error: { message: "" },
},
],
})
);
app = moduleRef.createNestApplication();
bootstrap(app as NestExpressApplication);
await app.init();
});

async function createOAuthClient(organizationId: number) {
const data = {
logo: "logo-url",
name: "name",
redirectUris: [CLIENT_REDIRECT_URI],
permissions: 32,
};
const secret = "secret";

const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
return client;
}

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

it(`POST /v2/destination-calendars: it should respond with a 200 returning back the user updated destination calendar`, async () => {
const body = {
integration: appleCalendarCredentials.type,
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
};

return request(app.getHttpServer())
.put("/v2/destination-calendars")
.set("Authorization", `Bearer ${accessTokenSecret}`)
.send(body)
.expect(200)
.then(async (response) => {
const responseBody: DestinationCalendarsOutputResponseDto = response.body;
expect(responseBody.status).toEqual(SUCCESS_STATUS);
expect(responseBody.data).toBeDefined();
expect(responseBody.data.credentialId).toEqual(appleCalendarCredentials.id);
expect(responseBody.data.integration).toEqual(body.integration);
expect(responseBody.data.externalId).toEqual(body.externalId);
expect(responseBody.data.userId).toEqual(user.id);
});
});

it(`POST /v2/destination-calendars: should fail 400 if calendar type is invalid`, async () => {
const body = {
integration: "not-supported-calendar",
externalId: "https://caldav.icloud.com/20961146906/calendars/83C4F9A1-F1D0-41C7-8FC3-0B$9AE22E813/",
};

return request(app.getHttpServer())
.put("/v2/destination-calendars")
.set("Authorization", `Bearer ${accessTokenSecret}`)
.send(body)
.expect(400);
});

afterAll(async () => {
await oauthClientRepositoryFixture.delete(oAuthClient.id);
await teamRepositoryFixture.delete(organization.id);
await userRepositoryFixture.deleteByEmail(user.email);
await app.close();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { DestinationCalendarsInputBodyDto } from "@/modules/destination-calendars/inputs/destination-calendars.input";
import {
DestinationCalendarsOutputDto,
DestinationCalendarsOutputResponseDto,
} from "@/modules/destination-calendars/outputs/destination-calendars.output";
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
import { UserWithProfile } from "@/modules/users/users.repository";
import { Body, Controller, Put, UseGuards } from "@nestjs/common";
import { ApiTags as DocsTags } from "@nestjs/swagger";
import { plainToClass } from "class-transformer";

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

@Controller({
path: "/v2/destination-calendars",
version: API_VERSIONS_VALUES,
})
@DocsTags("Destination-Calendars")
export class DestinationCalendarsController {
constructor(private readonly destinationCalendarsService: DestinationCalendarsService) {}

@Put("/")
@UseGuards(ApiAuthGuard)
@DocsTags("Select a third party destination calendar where events will be created")
async updateDestinationCalendars(
@Body() input: DestinationCalendarsInputBodyDto,
@GetUser() user: UserWithProfile
): Promise<DestinationCalendarsOutputResponseDto> {
const { integration, externalId } = input;
const updatedDestinationCalendar = await this.destinationCalendarsService.updateDestinationCalendars(
integration,
externalId,
user.id
);

return {
status: SUCCESS_STATUS,
data: plainToClass(DestinationCalendarsOutputDto, updatedDestinationCalendar, {
strategy: "excludeAll",
}),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
import { AppsRepository } from "@/modules/apps/apps.repository";
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
import { DestinationCalendarsController } from "@/modules/destination-calendars/controllers/destination-calendars.controller";
import { DestinationCalendarsRepository } from "@/modules/destination-calendars/destination-calendars.repository";
import { DestinationCalendarsService } from "@/modules/destination-calendars/services/destination-calendars.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { UsersRepository } from "@/modules/users/users.repository";
import { Module } from "@nestjs/common";

@Module({
imports: [PrismaModule],
providers: [
CalendarsRepository,
CalendarsService,
DestinationCalendarsService,
DestinationCalendarsRepository,
UsersRepository,
CredentialsRepository,
AppsRepository,
],
controllers: [DestinationCalendarsController],
exports: [DestinationCalendarsRepository],
})
export class DestinationCalendarsModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";

@Injectable()
export class DestinationCalendarsRepository {
constructor(private readonly dbWrite: PrismaWriteService) {}

async updateCalendar(
integration: string,
externalId: string,
credentialId: number,
userId: number,
primaryEmail: string | null
) {
return await this.dbWrite.prisma.destinationCalendar.upsert({
update: {
integration,
externalId,
credentialId,
primaryEmail,
},
create: {
integration,
externalId,
credentialId,
primaryEmail,
userId,
},
where: {
userId: userId,
},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose } from "class-transformer";
import { IsString, IsEnum } from "class-validator";

import {
APPLE_CALENDAR_TYPE,
GOOGLE_CALENDAR_TYPE,
OFFICE_365_CALENDAR_TYPE,
} from "@calcom/platform-constants";

export class DestinationCalendarsInputBodyDto {
@IsString()
@Expose()
@ApiProperty({
example: APPLE_CALENDAR_TYPE,
description: "The calendar service you want to integrate, as returned by the /calendars endpoint",
enum: [APPLE_CALENDAR_TYPE, GOOGLE_CALENDAR_TYPE, OFFICE_365_CALENDAR_TYPE],
required: true,
})
@IsEnum([APPLE_CALENDAR_TYPE, GOOGLE_CALENDAR_TYPE, OFFICE_365_CALENDAR_TYPE])
readonly integration!:
| typeof APPLE_CALENDAR_TYPE
| typeof GOOGLE_CALENDAR_TYPE
| typeof OFFICE_365_CALENDAR_TYPE;

@IsString()
@Expose()
@ApiProperty({
example: "https://caldav.icloud.com/26962146906/calendars/1644422A-1945-4438-BBC0-4F0Q23A57R7S/",
description:
"Unique identifier used to represent the specfic calendar, as returned by the /calendars endpoint",
type: "string",
required: true,
})
readonly externalId!: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ApiProperty } from "@nestjs/swagger";
import { Expose, Type } from "class-transformer";
import { IsInt, IsString, ValidateNested, IsEnum } from "class-validator";

import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";

export class DestinationCalendarsOutputDto {
@IsInt()
@Expose()
readonly userId!: number;

@IsString()
@Expose()
readonly integration!: string;

@IsString()
@Expose()
readonly externalId!: string;

@IsInt()
@Expose()
readonly credentialId!: number | null;
}

export class DestinationCalendarsOutputResponseDto {
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;

@Expose()
@ValidateNested()
@Type(() => DestinationCalendarsOutputDto)
data!: DestinationCalendarsOutputDto;
}
Loading

0 comments on commit e7c2c8b

Please sign in to comment.