-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' into refactor_allow_managed_event_tests_parallel_…
…runs
- Loading branch information
Showing
70 changed files
with
3,019 additions
and
323 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
...rc/modules/destination-calendars/controllers/destination-calendars.controller.e2e-spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
46 changes: 46 additions & 0 deletions
46
.../api/v2/src/modules/destination-calendars/controllers/destination-calendars.controller.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}), | ||
}; | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
apps/api/v2/src/modules/destination-calendars/destination-calendars.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
34 changes: 34 additions & 0 deletions
34
apps/api/v2/src/modules/destination-calendars/destination-calendars.repository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}); | ||
} | ||
} |
36 changes: 36 additions & 0 deletions
36
apps/api/v2/src/modules/destination-calendars/inputs/destination-calendars.input.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
34 changes: 34 additions & 0 deletions
34
apps/api/v2/src/modules/destination-calendars/outputs/destination-calendars.output.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.