Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da67be475f |
@@ -27,12 +27,6 @@ import { User } from "@prisma/client";
|
||||
|
||||
- Use React children and context instead of passing props through multiple components
|
||||
|
||||
### ORM and Types
|
||||
|
||||
- Never import `@calcom/prisma/client` in features, services, UI, or handlers.
|
||||
- Use repository DTOs or domain types instead.
|
||||
- See "Repository + DTO Pattern and Method Conventions" in the knowledge base for details and examples.
|
||||
|
||||
### Security Rules
|
||||
|
||||
```typescript
|
||||
@@ -28,17 +28,11 @@
|
||||
|
||||
- `yarn test` - Run unit tests (vitest)
|
||||
- `yarn test <filename>` - Run tests for specific file
|
||||
- `yarn test <filename> -t "<testName>"` - Run specific test by name for specific file
|
||||
- `yarn test <filename> -- --integrationTestsOnly` - Run integration tests for specific file
|
||||
- `yarn test <filename> -t "<testName>" -- --integrationTestsOnly` - Run specific test by name
|
||||
- `yarn tdd` - Run tests in watch mode
|
||||
- `yarn test:ui` - Run tests with UI interface
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- `yarn test -- --integrationTestsOnly` - Run integration tests (vitest)
|
||||
- `yarn test <filename> -- --integrationTestsOnly` - Run integration tests for specific file
|
||||
- `yarn test <filename> -t "<testName>" -- --integrationTestsOnly` - Run specific integration test by name for specific file
|
||||
|
||||
|
||||
### End-to-End Tests
|
||||
|
||||
- `yarn e2e` - Run end-to-end tests (Playwright)
|
||||
@@ -225,38 +225,16 @@ throw new Error("Booking failed");
|
||||
|
||||
### Error Types
|
||||
|
||||
Use `ErrorWithCode` for files that are not directly coupled to tRPC. The tRPC package has a middleware called `errorConversionMiddleware` that automatically converts `ErrorWithCode` instances into `TRPCError` instances.
|
||||
|
||||
```typescript
|
||||
// ✅ Good - Use ErrorWithCode in non-tRPC files (services, repositories, utilities)
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { ErrorWithCode } from "@calcom/lib/errors";
|
||||
|
||||
// Option 1: Using constructor with ErrorCode enum
|
||||
throw new ErrorWithCode(ErrorCode.BookingNotFound, "Booking not found");
|
||||
|
||||
// Option 2: Using the Factory pattern for common HTTP errors
|
||||
throw ErrorWithCode.Factory.Forbidden("You don't have permission to view this");
|
||||
throw ErrorWithCode.Factory.NotFound("Resource not found");
|
||||
throw ErrorWithCode.Factory.BadRequest("Invalid input");
|
||||
|
||||
// ✅ Good - Use TRPCError only in tRPC routers/procedures
|
||||
// ✅ Good - Use proper error classes
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Invalid booking time slot",
|
||||
});
|
||||
|
||||
// ❌ Bad - Using TRPCError in non-tRPC files
|
||||
import { TRPCError } from "@trpc/server";
|
||||
// Don't use TRPCError in services, repositories, or utility files
|
||||
```
|
||||
|
||||
### packages/features Import Restrictions
|
||||
|
||||
Files in `packages/features/**` should NOT import from `trpc`. This keeps the features package decoupled from the tRPC layer, making the code more reusable and testable. Use `ErrorWithCode` for error handling in these files, and let the tRPC middleware handle the conversion.
|
||||
|
||||
## Basic Performance Guidelines
|
||||
|
||||
- Aim for O(n) or O(n log n) complexity, avoid O(n²)
|
||||
@@ -328,54 +306,9 @@ import { Button } from "@calcom/ui/components/button";
|
||||
- **Tests**: Same as source file + `.test.ts` or `.spec.ts`
|
||||
- **Avoid**: Dot-suffixes like `.service.ts`, `.repository.ts` (except for tests, types, specs)
|
||||
|
||||
## Repository + DTO Pattern and Method Conventions
|
||||
## Repository Method Conventions
|
||||
|
||||
We use a Repository + DTO pattern to isolate Prisma and the database from business logic. Repositories are the only layer that talks to Prisma and database models. All services, tRPC handlers, API controllers, workflows, and UI should depend on DTOs or domain types, not Prisma types.
|
||||
|
||||
DTOs (Data Transfer Objects) are simple TypeScript types or interfaces that represent our domain data in an ORM-agnostic way and are returned from repositories.
|
||||
|
||||
### Why this pattern?
|
||||
|
||||
- **Prevent type leaks**: Prisma type changes stay inside repositories instead of breaking services, handlers, and UI across dozens of files.
|
||||
- **Enable safe refactors**: Business logic works against stable DTOs, so you can change storage details without touching higher layers.
|
||||
- **Keep ORM swappable**: If we ever change ORM, only repository implementations change; services and handlers stay the same.
|
||||
|
||||
### What not to do: tight Prisma coupling in business logic
|
||||
|
||||
Do not import Prisma types in business logic. Prisma types belong only in the data access layer.
|
||||
|
||||
```typescript
|
||||
// ❌ Bad – business logic depends on Prisma
|
||||
import type { Webhook } from "@calcom/prisma/client";
|
||||
export function sendPayload(webhook: Webhook) { /* ... */ }
|
||||
|
||||
// ✅ Good – business logic depends on DTO + repository
|
||||
export interface WebhookDTO { id: string; url: string; }
|
||||
export interface IWebhookRepository { findById(id: string): Promise<WebhookDTO | null>; }
|
||||
export async function sendPayload(repo: IWebhookRepository, id: string) {
|
||||
const webhook = await repo.findById(id);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### DTOs in repositories
|
||||
|
||||
Repositories should expose DTOs from their public methods and keep Prisma types internal. Inside the repository, use `Prisma.*GetPayload` and map rows to DTOs. The public methods should only return DTOs, never Prisma models.
|
||||
|
||||
```typescript
|
||||
// DTO – ORM-agnostic type exported from the repository module
|
||||
export interface BookingDTO {
|
||||
id: string;
|
||||
userId: number;
|
||||
startTime: Date;
|
||||
status: "PENDING" | "CONFIRMED" | "CANCELLED";
|
||||
}
|
||||
```
|
||||
|
||||
### Prisma boundaries
|
||||
|
||||
- **Allowed**: `packages/prisma`, repository implementations (`packages/features/**/repositories/*Repository.ts`), and low-level data access infrastructure.
|
||||
- **Not allowed**: `packages/features/**` business logic (non-repository), `packages/trpc/**` handlers, `apps/web/**`, `apps/api/v2/**` services/controllers, and workflow/webhook/service layers.
|
||||
Repositories should follow consistent naming and design patterns to promote reusability and maintainability.
|
||||
|
||||
### Method Naming Rules
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
feat: Booker accepts rrHostSubsetIds to specify round robin hosts
|
||||
@@ -0,0 +1,187 @@
|
||||
language: en-US
|
||||
tone_instructions: ''
|
||||
early_access: false
|
||||
enable_free_tier: true
|
||||
reviews:
|
||||
profile: chill
|
||||
request_changes_workflow: false
|
||||
high_level_summary: false
|
||||
high_level_summary_placeholder: '@coderabbitai summary'
|
||||
high_level_summary_in_walkthrough: false
|
||||
auto_title_placeholder: '@coderabbitai'
|
||||
auto_title_instructions: ''
|
||||
review_status: true
|
||||
commit_status: true
|
||||
fail_commit_status: false
|
||||
collapse_walkthrough: false
|
||||
changed_files_summary: false
|
||||
sequence_diagrams: false
|
||||
estimate_code_review_effort: false
|
||||
assess_linked_issues: true
|
||||
related_issues: false
|
||||
related_prs: true
|
||||
suggested_labels: false
|
||||
auto_apply_labels: false
|
||||
suggested_reviewers: false
|
||||
auto_assign_reviewers: false
|
||||
poem: false
|
||||
labeling_instructions: []
|
||||
path_filters:
|
||||
- '!**/node_modules/**'
|
||||
- '!**/dist/**'
|
||||
- '!**/build/**'
|
||||
- '!**/.next/**'
|
||||
path_instructions:
|
||||
- path: "**/*.{ts,tsx,js,jsx}"
|
||||
instructions: "Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module."
|
||||
abort_on_close: true
|
||||
disable_cache: false
|
||||
auto_review:
|
||||
enabled: true
|
||||
auto_incremental_review: true
|
||||
ignore_title_keywords: []
|
||||
labels: []
|
||||
drafts: false
|
||||
base_branches: []
|
||||
finishing_touches:
|
||||
docstrings:
|
||||
enabled: true
|
||||
unit_tests:
|
||||
enabled: true
|
||||
pre_merge_checks:
|
||||
docstrings:
|
||||
mode: warning
|
||||
threshold: 80
|
||||
title:
|
||||
mode: warning
|
||||
requirements: ''
|
||||
description:
|
||||
mode: warning
|
||||
issue_assessment:
|
||||
mode: warning
|
||||
tools:
|
||||
ast-grep:
|
||||
rule_dirs: []
|
||||
util_dirs: []
|
||||
essential_rules: true
|
||||
packages: []
|
||||
shellcheck:
|
||||
enabled: true
|
||||
ruff:
|
||||
enabled: true
|
||||
markdownlint:
|
||||
enabled: true
|
||||
github-checks:
|
||||
enabled: true
|
||||
timeout_ms: 180000
|
||||
languagetool:
|
||||
enabled: true
|
||||
enabled_rules: []
|
||||
disabled_rules: []
|
||||
enabled_categories: []
|
||||
disabled_categories: []
|
||||
enabled_only: false
|
||||
level: default
|
||||
biome:
|
||||
enabled: true
|
||||
hadolint:
|
||||
enabled: true
|
||||
swiftlint:
|
||||
enabled: true
|
||||
phpstan:
|
||||
enabled: true
|
||||
level: default
|
||||
phpmd:
|
||||
enabled: true
|
||||
phpcs:
|
||||
enabled: true
|
||||
golangci-lint:
|
||||
enabled: true
|
||||
yamllint:
|
||||
enabled: true
|
||||
gitleaks:
|
||||
enabled: true
|
||||
checkov:
|
||||
enabled: true
|
||||
detekt:
|
||||
enabled: true
|
||||
eslint:
|
||||
enabled: true
|
||||
flake8:
|
||||
enabled: true
|
||||
rubocop:
|
||||
enabled: true
|
||||
buf:
|
||||
enabled: true
|
||||
regal:
|
||||
enabled: true
|
||||
actionlint:
|
||||
enabled: true
|
||||
pmd:
|
||||
enabled: true
|
||||
cppcheck:
|
||||
enabled: true
|
||||
semgrep:
|
||||
enabled: true
|
||||
circleci:
|
||||
enabled: true
|
||||
clippy:
|
||||
enabled: true
|
||||
sqlfluff:
|
||||
enabled: true
|
||||
prismaLint:
|
||||
enabled: true
|
||||
pylint:
|
||||
enabled: true
|
||||
oxc:
|
||||
enabled: true
|
||||
shopifyThemeCheck:
|
||||
enabled: true
|
||||
luacheck:
|
||||
enabled: true
|
||||
brakeman:
|
||||
enabled: true
|
||||
dotenvLint:
|
||||
enabled: true
|
||||
htmlhint:
|
||||
enabled: true
|
||||
checkmake:
|
||||
enabled: true
|
||||
osvScanner:
|
||||
enabled: true
|
||||
chat:
|
||||
art: true
|
||||
auto_reply: true
|
||||
integrations:
|
||||
jira:
|
||||
usage: auto
|
||||
linear:
|
||||
usage: auto
|
||||
knowledge_base:
|
||||
opt_out: false
|
||||
web_search:
|
||||
enabled: true
|
||||
code_guidelines:
|
||||
enabled: true
|
||||
filePatterns: []
|
||||
learnings:
|
||||
scope: auto
|
||||
issues:
|
||||
scope: auto
|
||||
jira:
|
||||
usage: auto
|
||||
project_keys: []
|
||||
linear:
|
||||
usage: auto
|
||||
team_keys: []
|
||||
pull_requests:
|
||||
scope: auto
|
||||
mcp:
|
||||
usage: auto
|
||||
disabled_servers: []
|
||||
code_generation:
|
||||
docstrings:
|
||||
language: en-US
|
||||
path_instructions: []
|
||||
unit_tests:
|
||||
path_instructions: []
|
||||
@@ -1,36 +0,0 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
**/node_modules
|
||||
|
||||
# Build outputs
|
||||
.next
|
||||
**/.next
|
||||
dist
|
||||
**/dist
|
||||
build
|
||||
**/build
|
||||
|
||||
# Git
|
||||
.git
|
||||
.github
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
**/coverage
|
||||
test-results
|
||||
**/test-results
|
||||
.turbo
|
||||
**/.turbo
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Examples and docs
|
||||
docs
|
||||
+1
-25
@@ -127,14 +127,6 @@ NEXT_PUBLIC_HELPSCOUT_KEY=
|
||||
NEXT_PUBLIC_FRESHCHAT_TOKEN=
|
||||
NEXT_PUBLIC_FRESHCHAT_HOST=
|
||||
|
||||
# For holiday feature:
|
||||
# Step-by-step: Get a Google Calendar API Key
|
||||
# 1. Go to Google Cloud Console: https://console.cloud.google.com/
|
||||
# 2. Select or Create a Project
|
||||
# 3. Enable Google Calendar API (APIs & Services → Library , Search for Google Calendar API)
|
||||
# 4. Create the API Key (APIs & Services → Credentials)
|
||||
GOOGLE_CALENDAR_API_KEY=
|
||||
|
||||
# Google OAuth credentials
|
||||
# To enable Login with Google you need to:
|
||||
# 1. Set `GOOGLE_API_CREDENTIALS` below
|
||||
@@ -222,8 +214,6 @@ STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_PRODUCT_ID=
|
||||
# Used to pass trial days for orgs during the Stripe checkout session
|
||||
STRIPE_ORG_TRIAL_DAYS=
|
||||
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
@@ -433,8 +423,7 @@ VAPID_PRIVATE_KEY=
|
||||
|
||||
# Mintlify chat api
|
||||
# Power AI chat in for docs
|
||||
MINTLIFY_CHAT_API_KEY=
|
||||
NEXT_PUBLIC_ENABLE_MINTLIFY_CHAT=
|
||||
NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY=
|
||||
NEXT_PUBLIC_CHAT_API_URL=
|
||||
NEXT_PUBLIC_DOCS_URL=
|
||||
|
||||
@@ -497,16 +486,3 @@ CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY=
|
||||
SALESFORCE_GRAPHQL_DELAY_MS=500
|
||||
SALESFORCE_GRAPHQL_MAX_DELAY_MS=2000
|
||||
SALESFORCE_GRAPHQL_MAX_RETRIES=3
|
||||
|
||||
# Force Node.js to use UTC as process timezone
|
||||
TZ=UTC
|
||||
|
||||
# test oauth client for `packages/platform/examples/base` (optional)
|
||||
SEED_PLATFORM_OAUTH_CLIENT_ID=
|
||||
SEED_PLATFORM_OAUTH_CLIENT_SECRET=
|
||||
|
||||
# Trigger.dev
|
||||
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
|
||||
TRIGGER_SECRET_KEY=
|
||||
TRIGGER_API_URL=https://api.trigger.dev
|
||||
TRIGGER_DEV_PROJECT_REF=
|
||||
@@ -1,4 +0,0 @@
|
||||
/.yarn/** linguist-vendored
|
||||
/.yarn/releases/* binary
|
||||
/.yarn/plugins/**/* binary
|
||||
/.pnp.* binary linguist-generated
|
||||
@@ -1,5 +1,6 @@
|
||||
/**/package.json @calcom/Foundation
|
||||
/apps/api/**/* @calcom/API
|
||||
/apps/ui-playground/**/* @calcom/Consumer @calcom/Foundation
|
||||
/apps/web/**/layout.tsx @calcom/Consumer @calcom/Foundation
|
||||
/apps/web/lib/csp.ts @calcom/Foundation
|
||||
/apps/web/lib/buildNonce.ts @calcom/Foundation
|
||||
|
||||
@@ -26,7 +26,7 @@ runs:
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ inputs.path }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}
|
||||
- run: yarn db-seed
|
||||
- run: echo ${{ env.E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS }} && yarn db-seed
|
||||
if: steps.cache-db.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- name: Postgres Dump Backup
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
name: "Docker Build and Test"
|
||||
description: "Reusable action to build and test Docker images for Cal.com"
|
||||
|
||||
inputs:
|
||||
platform:
|
||||
description: "Target platform (linux/amd64 or arm64)"
|
||||
required: true
|
||||
platform-suffix:
|
||||
description: "Suffix to add to image tags (e.g., -arm)"
|
||||
required: false
|
||||
default: ""
|
||||
dockerhub-username:
|
||||
description: "Docker Hub username"
|
||||
required: true
|
||||
dockerhub-token:
|
||||
description: "Docker Hub token"
|
||||
required: true
|
||||
github-token:
|
||||
description: "GitHub token for GHCR"
|
||||
required: true
|
||||
postgres-user:
|
||||
description: "PostgreSQL user"
|
||||
required: true
|
||||
postgres-password:
|
||||
description: "PostgreSQL password"
|
||||
required: true
|
||||
postgres-db:
|
||||
description: "PostgreSQL database name"
|
||||
required: true
|
||||
database-host:
|
||||
description: "Database host"
|
||||
required: true
|
||||
push-image:
|
||||
description: "Whether to push the built image"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Log in to the Docker Hub registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ inputs.dockerhub-username }}
|
||||
password: ${{ inputs.dockerhub-token }}
|
||||
logout: true
|
||||
|
||||
- name: Log in to the Github Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ inputs.github-token }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
docker.io/calendso/calendso
|
||||
docker.io/calcom/cal.com
|
||||
ghcr.io/calcom/cal.com
|
||||
flavor: |
|
||||
latest=${{ !github.event.release.prerelease }}
|
||||
suffix=${{ inputs.platform-suffix }}
|
||||
|
||||
- name: Copy env
|
||||
shell: bash
|
||||
run: |
|
||||
grep -o '^[^#]*' .env.example > .env
|
||||
cat .env >> $GITHUB_ENV
|
||||
echo "DATABASE_HOST=localhost:5432" >> $GITHUB_ENV
|
||||
eval $(sed -e '/^#/d' -e 's/^/export /' -e 's/$/;/' .env) ;
|
||||
|
||||
- name: Start database
|
||||
shell: bash
|
||||
run: |
|
||||
docker compose up -d database
|
||||
|
||||
- name: Show database logs and container status
|
||||
shell: bash
|
||||
run: |
|
||||
echo "--- Container Status ---"
|
||||
docker compose ps database
|
||||
|
||||
echo "--- Container Logs ---"
|
||||
docker compose logs database
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver-opts: |
|
||||
network=container:database
|
||||
buildkitd-flags: |
|
||||
--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host
|
||||
|
||||
- name: Build image
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
load: true
|
||||
push: false
|
||||
platforms: ${{ inputs.platform }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_V2_URL=http://localhost:5555/api/v2
|
||||
NEXT_PUBLIC_LICENSE_CONSENT=agree
|
||||
DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||
DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||
|
||||
- name: Test runtime
|
||||
shell: bash
|
||||
run: |
|
||||
tags="${{ steps.meta.outputs.tags }}"
|
||||
IFS=',' read -ra ADDR <<< "$tags"
|
||||
tag=${ADDR[0]}
|
||||
|
||||
docker run --rm --network stack \
|
||||
-p 3000:3000 \
|
||||
-e DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@database/${{ inputs.postgres-db }} \
|
||||
-e DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@database/${{ inputs.postgres-db }} \
|
||||
-e NEXTAUTH_SECRET=${{ env.NEXTAUTH_SECRET }} \
|
||||
-e CALENDSO_ENCRYPTION_KEY=${{ env.CALENDSO_ENCRYPTION_KEY }} \
|
||||
$tag &
|
||||
|
||||
server_pid=$!
|
||||
|
||||
echo "Waiting for the server to start..."
|
||||
sleep 120
|
||||
|
||||
echo http://localhost:3000/auth/login
|
||||
|
||||
for i in {1..60}; do
|
||||
echo "Checking server health ($i/60)..."
|
||||
response=$(curl -o /dev/null -s -w "%{http_code}" http://localhost:3000/auth/login)
|
||||
echo "HTTP Status Code: $response"
|
||||
if [[ "$response" == "200" ]] || [[ "$response" == "307" ]]; then
|
||||
echo "Server is healthy"
|
||||
kill $server_pid
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Server health check failed"
|
||||
kill $server_pid
|
||||
exit 1
|
||||
env:
|
||||
NEXTAUTH_SECRET: "EI4qqDpcfdvf4A+0aQEEx8JjHxHSy4uWiZw/F32K+pA="
|
||||
CALENDSO_ENCRYPTION_KEY: "0zfLtY99wjeLnsM7qsa8xsT+Q0oSgnOL"
|
||||
|
||||
- name: Push image
|
||||
id: docker_push
|
||||
uses: docker/build-push-action@v6
|
||||
if: ${{ inputs.push-image == 'true' && !github.event.release.prerelease }}
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
platforms: ${{ inputs.platform }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: |
|
||||
NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_V2_URL=http://localhost:5555/api/v2
|
||||
NEXT_PUBLIC_LICENSE_CONSENT=agree
|
||||
DATABASE_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||
DATABASE_DIRECT_URL=postgresql://${{ inputs.postgres-user }}:${{ inputs.postgres-password }}@${{ inputs.database-host }}/${{ inputs.postgres-db }}
|
||||
|
||||
- name: Image digest
|
||||
shell: bash
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
|
||||
- name: Cleanup
|
||||
shell: bash
|
||||
if: always()
|
||||
run: |
|
||||
docker compose down
|
||||
@@ -1,4 +0,0 @@
|
||||
GET /v2/event-types/{eventTypeId} added to the 'data' response property 'oneOf' list for the response status '200'
|
||||
GET /v2/event-types/{eventTypeId} added '#/components/schemas/EventTypeOutput_2024_06_14, #/components/schemas/TeamEventTypeOutput_2024_06_14' to the 'data' response property 'oneOf' list for the response status '200'
|
||||
GET /v2/bookings/{bookingUid}/recordings added the new required 'header' request parameter 'Authorization'
|
||||
GET /v2/bookings/{bookingUid}/transcripts added the new required 'header' request parameter 'Authorization'
|
||||
@@ -56,8 +56,8 @@ jobs:
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
yarn workspace @calcom/api-v2 run generate-schemas &&
|
||||
rm -rf apps/api/v2/node_modules &&
|
||||
yarn install &&
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
shell: bash
|
||||
|
||||
@@ -6,31 +6,23 @@ on:
|
||||
- main
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
permissions:
|
||||
id-token: write # for NPM provenance - developers can verify where and how it was built
|
||||
contents: write # to create release by changesets/action below
|
||||
pull-requests: write # to create pull request by changesets/action below
|
||||
|
||||
permissions: {} # remove permissions and set explicitly under "permissions:" below
|
||||
jobs:
|
||||
release:
|
||||
if: github.repository == 'calcom/cal.com' # prevent this action from running on forks
|
||||
permissions:
|
||||
id-token: write # for NPM provenance - developers can verify where and how it was built
|
||||
contents: write # to create release by changesets/action below
|
||||
pull-requests: write # to create pull request by changesets/action below
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Node and registry for OIDC
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.17.0"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
always-auth: false
|
||||
- name: Setup node, yarn and install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Install npm at version supporting OIDC
|
||||
run: npm install -g npm@11.5.1
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
@@ -41,7 +33,8 @@ jobs:
|
||||
title: "chore: version packages" # PR title
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_CONFIG_PROVENANCE: "true"
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
VITE_BOOKER_EMBED_OAUTH_CLIENT_ID: ${{ secrets.VITE_BOOKER_EMBED_OAUTH_CLIENT_ID }}
|
||||
VITE_BOOKER_EMBED_API_URL: ${{ secrets.VITE_BOOKER_EMBED_API_URL }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
|
||||
NEXT_PUBLIC_WEBAPP_URL: ${{ secrets.NEXT_PUBLIC_WEBAPP_URL }}
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: calcom/test-results-2
|
||||
repository: calcom/test-results
|
||||
ref: gh-pages
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: calcom/test-results-2
|
||||
repository: calcom/test-results
|
||||
ref: gh-pages
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Set Git User
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
name: Companion Builds
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
EXPO_PUBLIC_CAL_API_KEY: ${{ secrets.CI_EXPO_PUBLIC_CAL_API_KEY || vars.CI_EXPO_PUBLIC_CAL_API_KEY || vars.EXPO_PUBLIC_CAL_API_KEY }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Companion App & Extension
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.3.0
|
||||
cache: true
|
||||
cache-dependency-path: companion/bun.lock
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: companion
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build Expo web app
|
||||
working-directory: companion
|
||||
env:
|
||||
EXPO_NO_TELEMETRY: 1
|
||||
CI: true
|
||||
run: bunx expo export --platform web
|
||||
|
||||
- name: Build browser extension
|
||||
working-directory: companion
|
||||
run: bun run ext:build
|
||||
@@ -20,8 +20,8 @@ jobs:
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 7
|
||||
days-before-pr-close: 14
|
||||
stale-pr-message: "This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active."
|
||||
close-pr-message: "This PR has been closed due to inactivity. Please feel free to reopen it if you'd like to continue the work."
|
||||
days-before-pr-stale: 14
|
||||
days-before-pr-close: -1
|
||||
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||
operations-per-run: 100
|
||||
|
||||
@@ -72,22 +72,18 @@ jobs:
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
- uses: ./.github/actions/cache-db
|
||||
|
||||
|
||||
- name: Generate Swagger
|
||||
working-directory: apps/api/v2
|
||||
run: yarn generate-swagger
|
||||
|
||||
- name: Check breaking changes
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v "$PWD:/work" -w /work \
|
||||
tufin/oasdiff:latest \
|
||||
breaking \
|
||||
https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json \
|
||||
docs/api-reference/v2/openapi.json \
|
||||
--fail-on ERR \
|
||||
--err-ignore .github/oasdiff-err-ignore.txt
|
||||
|
||||
uses: oasdiff/oasdiff-action/breaking@main
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/calcom/cal.com/refs/heads/main/docs/api-reference/v2/openapi.json
|
||||
revision: docs/api-reference/v2/openapi.json
|
||||
fail-on: ERR
|
||||
|
||||
- name: Run Tests
|
||||
working-directory: apps/api/v2
|
||||
run: |
|
||||
@@ -95,7 +91,7 @@ jobs:
|
||||
EXIT_CODE=$?
|
||||
echo "yarn workspace @calcom/platform-libraries build && yarn test:e2e command exit code: $EXIT_CODE"
|
||||
exit $EXIT_CODE
|
||||
|
||||
|
||||
- name: Upload Test Results
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -5,7 +5,6 @@ permissions:
|
||||
contents: read
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
INTEGRATION_TESTS: "true"
|
||||
ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }}
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
|
||||
@@ -15,6 +15,18 @@ jobs:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
team-labels:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: equitybee/team-label-action@main
|
||||
with:
|
||||
repo-token: ${{ secrets.EQUITY_BEE_TEAM_LABELER_ACTION_TOKEN }}
|
||||
organization-name: calcom
|
||||
ignore-labels: "admin, app-store, ai, api, authentication, automated-testing, billing, bookings, caldav, calendar-apps, ci, console, crm-apps, dba, devops, docs, documentation, emails, embeds, event-types, i18n, impersonation, manual-testing, ui, performance, ops-stack, organizations, public-api, routing-forms, seats, self-hosted, sre, teams, webhooks, workflows, zapier"
|
||||
apply-labels-from-issue:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ env:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
E2E_TEST_MAILHOG_ENABLED: ${{ vars.E2E_TEST_MAILHOG_ENABLED }}
|
||||
|
||||
@@ -146,13 +146,6 @@ jobs:
|
||||
uses: ./.github/workflows/docs-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build-companion:
|
||||
name: Companion builds
|
||||
needs: [changes, check-label, deps]
|
||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||
uses: ./.github/workflows/companion-build.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
name: Production builds
|
||||
needs: [changes, check-label, deps]
|
||||
@@ -251,7 +244,6 @@ jobs:
|
||||
build-api-v2,
|
||||
build-atoms,
|
||||
build-docs,
|
||||
build-companion,
|
||||
e2e,
|
||||
e2e-api-v2,
|
||||
e2e-embed,
|
||||
|
||||
@@ -10,6 +10,7 @@ env:
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }}
|
||||
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }}
|
||||
E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }}
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD: ${{ secrets.E2E_TEST_APPLE_CALENDAR_PASSWORD }}
|
||||
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
@@ -35,6 +36,7 @@ env:
|
||||
NEXT_PUBLIC_API_V2_ROOT_URL: ${{ secrets.CI_NEXT_PUBLIC_API_V2_ROOT_URL }}
|
||||
NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED: ${{ vars.CI_NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED }}
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Web App
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Checkout GitHub Pages Branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: calcom/test-results-2
|
||||
repository: calcom/test-results
|
||||
ref: gh-pages
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- name: Set Git User
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
- name: Output Report URL as Workflow Annotation
|
||||
id: url
|
||||
run: |
|
||||
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results-2/$HTML_REPORT_URL_PATH
|
||||
FULL_HTML_REPORT_URL=https://calcom.github.io/test-results/$HTML_REPORT_URL_PATH
|
||||
echo "::notice title=📋 Published Playwright Test Report::$FULL_HTML_REPORT_URL"
|
||||
echo "link=$FULL_HTML_REPORT_URL" >> $GITHUB_OUTPUT
|
||||
- name: Find Comment
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
name: "Release Docker"
|
||||
|
||||
env:
|
||||
POSTGRES_USER: "unicorn_user"
|
||||
POSTGRES_PASSWORD: "magical_password"
|
||||
POSTGRES_DB: "calendso"
|
||||
DATABASE_HOST: "database:5432"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
on: # yamllint disable-line rule:truthy
|
||||
release:
|
||||
types:
|
||||
- created
|
||||
- published
|
||||
# in case manual trigger is needed
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -17,9 +12,11 @@ on:
|
||||
description: "v{Major}.{Minor}.{Patch}"
|
||||
|
||||
jobs:
|
||||
release-amd64:
|
||||
name: "Release AMD64"
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
release:
|
||||
name: "Remote Release"
|
||||
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -27,84 +24,25 @@ jobs:
|
||||
- name: "Determine tag"
|
||||
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||
|
||||
- name: Build and test Docker image
|
||||
uses: ./.github/actions/docker-build-and-test
|
||||
- name: "Run remote release workflow"
|
||||
uses: "actions/github-script@v6"
|
||||
with:
|
||||
platform: "linux/amd64"
|
||||
platform-suffix: ""
|
||||
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
postgres-user: ${{ env.POSTGRES_USER }}
|
||||
postgres-password: ${{ env.POSTGRES_PASSWORD }}
|
||||
postgres-db: ${{ env.POSTGRES_DB }}
|
||||
database-host: ${{ env.DATABASE_HOST }}
|
||||
push-image: "true"
|
||||
# Requires a personal access token with Actions Read and write permissions on calcom/docker.
|
||||
github-token: "${{ secrets.DOCKER_REPO_ACCESS_TOKEN }}"
|
||||
script: |
|
||||
try {
|
||||
const response = await github.rest.actions.createWorkflowDispatch({
|
||||
owner: context.repo.owner,
|
||||
repo: 'docker',
|
||||
workflow_id: 'create-release.yaml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
"RELEASE_TAG": process.env.RELEASE_TAG
|
||||
},
|
||||
});
|
||||
|
||||
- name: Notify Slack on Success
|
||||
if: success()
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": ":large_green_circle: Workflow *${{ github.workflow }}* (AMD64) succeeded in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
console.log(response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
core.setFailed(error.message);
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Notify Slack on Failure
|
||||
if: failure()
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": ":red_circle: Workflow *${{ github.workflow }}* (AMD64) failed in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||
|
||||
release-arm:
|
||||
name: "Release ARM"
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: "Determine tag"
|
||||
run: 'echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV'
|
||||
|
||||
- name: Build and test Docker image
|
||||
uses: ./.github/actions/docker-build-and-test
|
||||
with:
|
||||
platform: "arm64"
|
||||
platform-suffix: "-arm"
|
||||
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
postgres-user: ${{ env.POSTGRES_USER }}
|
||||
postgres-password: ${{ env.POSTGRES_PASSWORD }}
|
||||
postgres-db: ${{ env.POSTGRES_DB }}
|
||||
database-host: ${{ env.DATABASE_HOST }}
|
||||
push-image: "true"
|
||||
|
||||
- name: Notify Slack on Success
|
||||
if: success()
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": ":large_green_circle: Workflow *${{ github.workflow }}* (ARM) succeeded in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||
|
||||
- name: Notify Slack on Failure
|
||||
if: failure()
|
||||
uses: slackapi/slack-github-action@v1.24.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"text": ":red_circle: Workflow *${{ github.workflow }}* (ARM) failed in job *${{ github.job }}*.\nSee: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.CI_SLACK_WEBHOOK_URL }}
|
||||
|
||||
@@ -67,9 +67,6 @@ tsconfig.tsbuildinfo
|
||||
|
||||
# Prisma generated files
|
||||
packages/prisma/client/*
|
||||
!packages/prisma/client/index.ts
|
||||
apps/api/v2/generated/prisma/*
|
||||
packages/prisma/generated/prisma/*
|
||||
packages/prisma/zod/**/*.ts
|
||||
packages/prisma/enums
|
||||
|
||||
@@ -105,6 +102,3 @@ packages/**/.yarn/ci-cache/
|
||||
|
||||
# AI
|
||||
.claude
|
||||
|
||||
# trigger.dev
|
||||
.trigger
|
||||
@@ -1,3 +1,6 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
||||
|
||||
yarn app-store:build && git add packages/app-store/*.generated.*
|
||||
|
||||
Vendored
+1
@@ -4,6 +4,7 @@
|
||||
"yzhang.markdown-all-in-one", // nicer markdown support
|
||||
"esbenp.prettier-vscode", // prettier plugin
|
||||
"dbaeumer.vscode-eslint", // eslint plugin
|
||||
"bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
|
||||
"ban.spellright", // Spell check for docs
|
||||
"stripe.vscode-stripe", // stripe VSCode extension
|
||||
"Prisma.prisma", // syntax|format|completion for prisma
|
||||
|
||||
Vendored
+1
-1
@@ -8,5 +8,5 @@
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"spellright.language": ["en"],
|
||||
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
|
||||
"tailwindCSS.classFunctions": ["cva"]
|
||||
"tailwindCSS.experimental.classRegex": [["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]]
|
||||
}
|
||||
|
||||
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
-2
@@ -1,2 +0,0 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
@@ -1,213 +1,30 @@
|
||||
# Cal.com Development Guide for AI Agents
|
||||
|
||||
You are a senior Cal.com engineer working in a Yarn/Turbo monorepo. You prioritize type safety, security, and small, reviewable diffs.
|
||||
**📖 Complete documentation is in the [.agents/](.agents/) directory.**
|
||||
|
||||
## Do
|
||||
## Quick Reference
|
||||
|
||||
- Use `select` instead of `include` in Prisma queries for performance and security
|
||||
- Use `import type { X }` for TypeScript type imports
|
||||
- Use early returns to reduce nesting: `if (!booking) return null;`
|
||||
- Use `ErrorWithCode` for errors in non-tRPC files (services, repositories, utilities); use `TRPCError` only in tRPC routers
|
||||
- Use conventional commits: `feat:`, `fix:`, `refactor:`
|
||||
- Create PRs in draft mode by default
|
||||
- Run `yarn type-check:ci --force` before concluding CI failures are unrelated to your changes
|
||||
- Import directly from source files, not barrel files (e.g., `@calcom/ui/components/button` not `@calcom/ui`)
|
||||
- Add translations to `apps/web/public/static/locales/en/common.json` for all UI strings
|
||||
- Use `date-fns` or native `Date` instead of Day.js when timezone awareness isn't needed
|
||||
- Put permission checks in `page.tsx`, never in `layout.tsx`
|
||||
- Use `ast-grep` for searching if available; otherwise use `rg` (ripgrep), then fall back to `grep`
|
||||
### Essential Commands
|
||||
|
||||
## Don't
|
||||
- `yarn dev` - Start development server
|
||||
- `yarn build` - Build all packages
|
||||
- `yarn lint:fix` - Lint and fix code
|
||||
- `yarn type-check` - TypeScript checking
|
||||
- `yarn test <filename> -- --integrationTestsOnly` - Run integration tests
|
||||
- `yarn e2e <filename> --grep "<testName>"` - Run specific E2E test
|
||||
|
||||
- Never use `as any` - use proper type-safe solutions instead
|
||||
- Never expose `credential.key` field in API responses or queries
|
||||
- Never commit secrets or API keys
|
||||
- Never modify `*.generated.ts` files directly - they're created by app-store-cli
|
||||
- Never put business logic in repositories - that belongs in Services
|
||||
- Never use barrel imports from index.ts files
|
||||
- Never skip running type checks before pushing
|
||||
- Never create large PRs (>500 lines or >10 files) - split them instead
|
||||
## Tool Preferences
|
||||
|
||||
## Commands
|
||||
### Search Tools Priority
|
||||
|
||||
### File-scoped (preferred for speed)
|
||||
Use tools in this order of preference:
|
||||
|
||||
```bash
|
||||
# Type check - always run on changed files
|
||||
yarn type-check:ci --force
|
||||
1. **ast-grep** - For AST-based code searches (if available)
|
||||
2. **rg (ripgrep)** - For fast text searches
|
||||
3. **grep** - As fallback for text searches
|
||||
|
||||
# Lint single file
|
||||
yarn eslint --fix path/to/file.tsx
|
||||
## 📚 Detailed Documentation
|
||||
|
||||
# Format single file
|
||||
yarn prettier --write path/to/file.tsx
|
||||
|
||||
# Unit test specific file
|
||||
yarn vitest run path/to/file.test.ts
|
||||
|
||||
# Unit test specific file + specific test
|
||||
yarn vitest run path/to/file.test.ts --testNamePattern="specific test name"
|
||||
|
||||
# Integration test specific file
|
||||
yarn test path/to/file.integration-test.ts -- --integrationTestsOnly
|
||||
|
||||
# Integration test specific file + specific test
|
||||
yarn test path/to/file.integration-test.ts --testNamePattern="specific test name" -- --integrationTestsOnly
|
||||
|
||||
# E2E test specific file
|
||||
PLAYWRIGHT_HEADLESS=1 yarn e2e path/to/file.e2e.ts
|
||||
|
||||
# E2E test specific file + specific test
|
||||
PLAYWRIGHT_HEADLESS=1 yarn e2e path/to/file.e2e.ts --grep "specific test name"
|
||||
```
|
||||
|
||||
### Project-wide (use sparingly)
|
||||
|
||||
```bash
|
||||
# Development
|
||||
yarn dev # Start dev server
|
||||
yarn dx # Dev with database setup
|
||||
|
||||
# Build & check
|
||||
yarn build # Build all packages
|
||||
yarn lint:fix # Lint and fix all
|
||||
yarn type-check # Type check all
|
||||
|
||||
# Tests (use TZ=UTC for consistency)
|
||||
TZ=UTC yarn test # All unit tests
|
||||
yarn e2e # All E2E tests
|
||||
|
||||
# Database
|
||||
yarn prisma generate # Regenerate types after schema changes
|
||||
yarn workspace @calcom/prisma db-migrate # Run migrations
|
||||
```
|
||||
|
||||
## Boundaries
|
||||
|
||||
### Always do
|
||||
- Run type check on changed files before committing
|
||||
- Run relevant tests before pushing
|
||||
- Use `select` in Prisma queries
|
||||
- Follow conventional commits for PR titles
|
||||
|
||||
### Ask first
|
||||
- Adding new dependencies
|
||||
- Schema changes to `packages/prisma/schema.prisma`
|
||||
- Changes affecting multiple packages
|
||||
- Deleting files
|
||||
- Running full build or E2E suites
|
||||
|
||||
### Never do
|
||||
- Commit secrets, API keys, or `.env` files
|
||||
- Expose `credential.key` in any query
|
||||
- Use `as any` type casting
|
||||
- Force push or rebase shared branches
|
||||
- Modify generated files directly
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
apps/web/ # Main Next.js application
|
||||
packages/prisma/ # Database schema (schema.prisma) and migrations
|
||||
packages/trpc/ # tRPC API layer (routers in server/routers/)
|
||||
packages/ui/ # Shared UI components
|
||||
packages/features/ # Feature-specific code
|
||||
packages/app-store/ # Third-party integrations
|
||||
packages/lib/ # Shared utilities
|
||||
```
|
||||
|
||||
### Key files
|
||||
- Routes: `apps/web/app/` (App Router)
|
||||
- Database schema: `packages/prisma/schema.prisma`
|
||||
- tRPC routers: `packages/trpc/server/routers/`
|
||||
- Translations: `apps/web/public/static/locales/en/common.json`
|
||||
- Workflow constants: `packages/features/ee/workflows/lib/constants.ts`
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Framework**: Next.js 13+ (App Router in some areas)
|
||||
- **Language**: TypeScript (strict)
|
||||
- **Database**: PostgreSQL with Prisma ORM
|
||||
- **API**: tRPC for type-safe APIs
|
||||
- **Auth**: NextAuth.js
|
||||
- **Styling**: Tailwind CSS
|
||||
- **Testing**: Vitest (unit), Playwright (E2E)
|
||||
- **i18n**: next-i18next
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Good error handling
|
||||
|
||||
```typescript
|
||||
// Good - Descriptive error with context
|
||||
throw new Error(`Unable to create booking: User ${userId} has no available time slots for ${date}`);
|
||||
|
||||
// Bad - Generic error
|
||||
throw new Error("Booking failed");
|
||||
```
|
||||
|
||||
For which error class to use (`ErrorWithCode` vs `TRPCError`) and concrete examples, see [Error Types in knowledge-base.md](agents/knowledge-base.md#error-types).
|
||||
|
||||
### Good Prisma query
|
||||
|
||||
```typescript
|
||||
// Good - Use select for performance and security
|
||||
const booking = await prisma.booking.findFirst({
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Bad - Include fetches all fields including sensitive ones
|
||||
const booking = await prisma.booking.findFirst({
|
||||
include: { user: true }
|
||||
});
|
||||
```
|
||||
|
||||
### Good imports
|
||||
|
||||
```typescript
|
||||
// Good - Type imports and direct paths
|
||||
import type { User } from "@prisma/client";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
// Bad - Regular import for types, barrel imports
|
||||
import { User } from "@prisma/client";
|
||||
import { Button } from "@calcom/ui";
|
||||
```
|
||||
|
||||
## PR Checklist
|
||||
|
||||
- [ ] Title follows conventional commits: `feat(scope): description`
|
||||
- [ ] Type check passes: `yarn type-check:ci --force`
|
||||
- [ ] Lint passes: `yarn lint:fix`
|
||||
- [ ] Relevant tests pass
|
||||
- [ ] Diff is small and focused (<500 lines, <10 files)
|
||||
- [ ] No secrets or API keys committed
|
||||
- [ ] UI strings added to translation files
|
||||
- [ ] Created as draft PR
|
||||
|
||||
## When Stuck
|
||||
|
||||
- Ask a clarifying question before making large speculative changes
|
||||
- Propose a short plan for complex tasks
|
||||
- Open a draft PR with notes if unsure about approach
|
||||
- Fix type errors before test failures - they're often the root cause
|
||||
- Run `yarn prisma generate` if you see missing enum/type errors
|
||||
|
||||
## Extended Documentation
|
||||
|
||||
For detailed information, see the `agents/` directory:
|
||||
|
||||
- **[agents/README.md](agents/README.md)** - Architecture overview and patterns
|
||||
- **[agents/commands.md](agents/commands.md)** - Complete command reference
|
||||
- **[agents/knowledge-base.md](agents/knowledge-base.md)** - Domain knowledge and best practices
|
||||
- **[agents/coding-standards.md](agents/coding-standards.md)** - Coding standards with examples
|
||||
- **[.agents/README.md](.agents/README.md)** - Complete development guide
|
||||
- **[.agents/commands.md](.agents/commands.md)** - All build, test & dev commands
|
||||
- **[.agents/knowledge-base.md](.agents/knowledge-base.md)** - Knowledge base & best practices
|
||||
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
FROM --platform=$BUILDPLATFORM node:20 AS builder
|
||||
|
||||
WORKDIR /calcom
|
||||
|
||||
## If we want to read any ENV variable from .env file, we need to first accept and pass it as an argument to the Dockerfile
|
||||
ARG NEXT_PUBLIC_LICENSE_CONSENT
|
||||
ARG NEXT_PUBLIC_WEBSITE_TERMS_URL
|
||||
ARG NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL
|
||||
ARG CALCOM_TELEMETRY_DISABLED
|
||||
ARG DATABASE_URL
|
||||
ARG NEXTAUTH_SECRET=secret
|
||||
ARG CALENDSO_ENCRYPTION_KEY=secret
|
||||
ARG MAX_OLD_SPACE_SIZE=6144
|
||||
ARG NEXT_PUBLIC_API_V2_URL
|
||||
ARG CSP_POLICY
|
||||
|
||||
## We need these variables as required by Next.js build to create rewrites
|
||||
ARG NEXT_PUBLIC_SINGLE_ORG_SLUG
|
||||
ARG ORGANIZATIONS_ENABLED
|
||||
|
||||
ENV NEXT_PUBLIC_WEBAPP_URL=http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER \
|
||||
NEXT_PUBLIC_API_V2_URL=$NEXT_PUBLIC_API_V2_URL \
|
||||
NEXT_PUBLIC_LICENSE_CONSENT=$NEXT_PUBLIC_LICENSE_CONSENT \
|
||||
NEXT_PUBLIC_WEBSITE_TERMS_URL=$NEXT_PUBLIC_WEBSITE_TERMS_URL \
|
||||
NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL=$NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL \
|
||||
CALCOM_TELEMETRY_DISABLED=$CALCOM_TELEMETRY_DISABLED \
|
||||
DATABASE_URL=$DATABASE_URL \
|
||||
DATABASE_DIRECT_URL=$DATABASE_URL \
|
||||
NEXTAUTH_SECRET=${NEXTAUTH_SECRET} \
|
||||
CALENDSO_ENCRYPTION_KEY=${CALENDSO_ENCRYPTION_KEY} \
|
||||
NEXT_PUBLIC_SINGLE_ORG_SLUG=$NEXT_PUBLIC_SINGLE_ORG_SLUG \
|
||||
ORGANIZATIONS_ENABLED=$ORGANIZATIONS_ENABLED \
|
||||
NODE_OPTIONS=--max-old-space-size=${MAX_OLD_SPACE_SIZE} \
|
||||
BUILD_STANDALONE=true \
|
||||
CSP_POLICY=$CSP_POLICY
|
||||
|
||||
COPY package.json yarn.lock .yarnrc.yml playwright.config.ts turbo.json i18n.json ./
|
||||
COPY .yarn ./.yarn
|
||||
COPY apps/web ./apps/web
|
||||
COPY apps/api/v2 ./apps/api/v2
|
||||
COPY packages ./packages
|
||||
COPY tests ./tests
|
||||
|
||||
RUN yarn config set httpTimeout 1200000
|
||||
RUN npx turbo prune --scope=@calcom/web --scope=@calcom/trpc --docker
|
||||
RUN yarn install
|
||||
# Build and make embed servable from web/public/embed folder
|
||||
RUN yarn workspace @calcom/trpc run build
|
||||
RUN yarn --cwd packages/embeds/embed-core workspace @calcom/embed-core run build
|
||||
RUN yarn --cwd apps/web workspace @calcom/web run build
|
||||
RUN rm -rf node_modules/.cache .yarn/cache apps/web/.next/cache
|
||||
|
||||
FROM node:20 AS builder-two
|
||||
|
||||
WORKDIR /calcom
|
||||
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
COPY package.json .yarnrc.yml turbo.json i18n.json ./
|
||||
COPY .yarn ./.yarn
|
||||
COPY --from=builder /calcom/yarn.lock ./yarn.lock
|
||||
COPY --from=builder /calcom/node_modules ./node_modules
|
||||
COPY --from=builder /calcom/packages ./packages
|
||||
COPY --from=builder /calcom/apps/web ./apps/web
|
||||
COPY --from=builder /calcom/packages/prisma/schema.prisma ./prisma/schema.prisma
|
||||
COPY scripts scripts
|
||||
RUN chmod +x scripts/*
|
||||
|
||||
# Save value used during this build stage. If NEXT_PUBLIC_WEBAPP_URL and BUILT_NEXT_PUBLIC_WEBAPP_URL differ at
|
||||
# run-time, then start.sh will find/replace static values again.
|
||||
ENV NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL \
|
||||
BUILT_NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL
|
||||
|
||||
RUN scripts/replace-placeholder.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_WEBAPP_URL}
|
||||
|
||||
FROM node:20 AS runner
|
||||
|
||||
WORKDIR /calcom
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends netcat-openbsd wget && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder-two /calcom ./
|
||||
ARG NEXT_PUBLIC_WEBAPP_URL=http://localhost:3000
|
||||
ENV NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL \
|
||||
BUILT_NEXT_PUBLIC_WEBAPP_URL=$NEXT_PUBLIC_WEBAPP_URL
|
||||
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=30s --retries=5 \
|
||||
CMD wget --spider http://localhost:3000 || exit 1
|
||||
|
||||
CMD ["/calcom/scripts/start.sh"]
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
Possible issue:
|
||||
- We won't be able to use already built calendar-cache when Delegation Credential is enabled because existing CalendarCache entries don't have userId set.
|
||||
|
||||
## Approach of always using SelectedCalendar.credentialId for CalendarCache even for DelegationCredentials
|
||||
- Delete Credentials when DelegationCredential is disabled
|
||||
|
||||
|
||||
TO Test
|
||||
- New members beyond the batch size are processed
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<img src="https://user-images.githubusercontent.com/8019099/210054112-5955e812-a76e-4160-9ddd-58f2c72f1cce.png" alt="Logo">
|
||||
</a>
|
||||
|
||||
<h3 align="center">Cal.com</h3>
|
||||
<h3 align="center">Cal.com (formerly Calendso)</h3>
|
||||
|
||||
<p align="center">
|
||||
The open-source Calendly successor.
|
||||
@@ -175,7 +175,6 @@ yarn dx
|
||||
```
|
||||
|
||||
#### Development tip
|
||||
|
||||
1. Add `export NODE_OPTIONS=“--max-old-space-size=16384”` to your shell script to increase the memory limit for the node process. Alternatively, you can run this in your terminal before running the app. Replace 16384 with the amount of RAM you want to allocate to the node process.
|
||||
|
||||
2. Add `NEXT_PUBLIC_LOGGER_LEVEL={level}` to your .env file to control the logging verbosity for all tRPC queries and mutations.\
|
||||
@@ -227,7 +226,7 @@ for Logger level to be set at info, for example.
|
||||
|
||||
3. Now open your psql shell with the DB you created: `psql -h localhost -U postgres -d <DB name>`
|
||||
|
||||
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
|
||||
4. Inside the psql shell execute `\conninfo`. And you will get the following info.
|
||||

|
||||
|
||||
5. Now extract all the info and add it to your DATABASE_URL. The url would look something like this
|
||||
@@ -376,268 +375,15 @@ Executable doesn't exist at /Users/alice/Library/Caches/ms-playwright/chromium-1
|
||||
|
||||
### Docker
|
||||
|
||||
**Official support**: Our team will begin to officially support the Dockerfile and docker-compose resources in this
|
||||
repository.
|
||||
The Docker configuration for Cal.com is an effort powered by people within the community.
|
||||
|
||||
**Important**: Cal.com will **not** be supporting installations that use these Docker resources. While we provide and maintain the Docker configurations, support for Docker-based installations is the responsibility of the user.
|
||||
If you want to contribute to the Docker repository, [reply here](https://github.com/calcom/docker/discussions/32).
|
||||
|
||||
This image can be found on DockerHub at [https://hub.docker.com/r/calcom/cal.com](https://hub.docker.com/r/calcom/cal.com).
|
||||
The Docker configuration can be found [in our docker repository](https://github.com/calcom/docker).
|
||||
|
||||
**Note for ARM Users**: Use the {version}-arm suffix for pulling images. Example: `docker pull calcom/cal.com:v5.6.19-arm`.
|
||||
Issues with Docker? Find your answer or open a new discussion [here](https://github.com/calcom/docker/discussions) to ask the community.
|
||||
|
||||
#### Requirements
|
||||
|
||||
Make sure you have `docker` & `docker compose` installed on the server / system. Both are installed by most docker utilities, including Docker Desktop and Rancher Desktop.
|
||||
|
||||
Note: `docker compose` without the hyphen is now the primary method of using docker-compose, per the Docker documentation.
|
||||
|
||||
#### (Most users) Running Cal.com with Docker Compose
|
||||
|
||||
If you are evaluating Cal.com or running with minimal to no modifications, this option is for you.
|
||||
|
||||
1. Clone calcom/cal.com
|
||||
|
||||
```bash
|
||||
git clone --recursive https://github.com/calcom/cal.com.git
|
||||
```
|
||||
|
||||
2. Change into the directory
|
||||
|
||||
```bash
|
||||
cd cal.com
|
||||
```
|
||||
|
||||
3. Prepare your configuration: Rename `.env.example` to `.env` and then update `.env`
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Most configurations can be left as-is, but for configuration options see [Important Run-time variables](#important-run-time-variables) below.
|
||||
|
||||
**Push Notifications (VAPID Keys)**
|
||||
If you see an error like:
|
||||
|
||||
```
|
||||
Error: No key set vapidDetails.publicKey
|
||||
```
|
||||
|
||||
This means your environment variables for Web Push are missing.
|
||||
You must generate and set `NEXT_PUBLIC_VAPID_PUBLIC_KEY` and `VAPID_PRIVATE_KEY`.
|
||||
|
||||
Generate them with:
|
||||
|
||||
```bash
|
||||
npx web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
Then update your `.env` file:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_VAPID_PUBLIC_KEY=your_public_key_here
|
||||
VAPID_PRIVATE_KEY=your_private_key_here
|
||||
```
|
||||
|
||||
Do **not** commit real keys to `.env.example` — only placeholders.
|
||||
|
||||
Update the appropriate values in your .env file, then proceed.
|
||||
|
||||
4. (optional) Pre-Pull the images by running the following command:
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
This will use the default image locations as specified by `image:` in the docker-compose.yaml file.
|
||||
|
||||
Note: To aid with support, by default Scarf.sh is used as registry proxy for download metrics.
|
||||
|
||||
5. Start Cal.com via docker compose
|
||||
|
||||
(Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run:
|
||||
|
||||
```bash
|
||||
docker compose up -d calcom studio
|
||||
```
|
||||
|
||||
To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run:
|
||||
|
||||
```bash
|
||||
docker compose up -d calcom
|
||||
```
|
||||
|
||||
**Note: to run in attached mode for debugging, remove `-d` from your desired run command.**
|
||||
|
||||
6. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go!
|
||||
|
||||
#### Updating Cal.com
|
||||
|
||||
1. Stop the Cal.com stack
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
2. Pull the latest changes
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
3. Update env vars as necessary.
|
||||
4. Re-start the Cal.com stack
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### (Advanced users) Build and Run Cal.com
|
||||
|
||||
1. Clone calcom/docker.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/calcom/cal.com.git calcom-docker
|
||||
```
|
||||
|
||||
2. Change into the directory
|
||||
|
||||
```bash
|
||||
cd calcom-docker
|
||||
```
|
||||
|
||||
3. Update the calcom submodule. This project depends on the Cal.com source code, which is included here as a Git submodule. To make sure you get everything you need, update the submodule with the command below.
|
||||
|
||||
```bash
|
||||
git submodule update --remote --init
|
||||
```
|
||||
|
||||
Note: DO NOT use recursive submodule update, otherwise you will receive a git authentication error.
|
||||
|
||||
4. Rename `.env.example` to `.env` and then update `.env`
|
||||
|
||||
For configuration options see [Build-time variables](#build-time-variables) below. Update the appropriate values in your .env file, then proceed.
|
||||
|
||||
5. Build the Cal.com docker image:
|
||||
|
||||
Note: Due to application configuration requirements, an available database is currently required during the build process.
|
||||
|
||||
a) If hosting elsewhere, configure the `DATABASE_URL` in the .env file, and skip the next step
|
||||
|
||||
b) If a local or temporary database is required, start a local database via docker compose.
|
||||
|
||||
```bash
|
||||
docker compose up -d database
|
||||
```
|
||||
|
||||
6. Build Cal.com via docker compose (DOCKER_BUILDKIT=0 must be provided to allow a network bridge to be used at build time. This requirement will be removed in the future)
|
||||
|
||||
```bash
|
||||
DOCKER_BUILDKIT=0 docker compose build calcom
|
||||
```
|
||||
|
||||
7. Start Cal.com via docker compose
|
||||
|
||||
(Most basic users, and for First Run) To run the complete stack, which includes a local Postgres database, Cal.com web app, and Prisma Studio:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
To run Cal.com web app and Prisma Studio against a remote database, ensure that DATABASE_URL is configured for an available database and run:
|
||||
|
||||
```bash
|
||||
docker compose up -d calcom studio
|
||||
```
|
||||
|
||||
To run only the Cal.com web app, ensure that DATABASE_URL is configured for an available database and run:
|
||||
|
||||
```bash
|
||||
docker compose up -d calcom
|
||||
```
|
||||
|
||||
**Note: to run in attached mode for debugging, remove `-d` from your desired run command.**
|
||||
|
||||
8. Open a browser to [http://localhost:3000](http://localhost:3000), or your defined NEXT_PUBLIC_WEBAPP_URL. The first time you run Cal.com, a setup wizard will initialize. Define your first user, and you're ready to go!
|
||||
|
||||
#### Configuration
|
||||
|
||||
##### Important Run-time variables
|
||||
|
||||
These variables must also be provided at runtime
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------- |
|
||||
| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` |
|
||||
| CALCOM_LICENSE_KEY | Enterprise License Key | optional | |
|
||||
| NEXT_PUBLIC_WEBAPP_URL | Base URL of the site. NOTE: if this value differs from the value used at build-time, there will be a slight delay during container start (to update the statically built files). | optional | `http://localhost:3000` |
|
||||
| NEXTAUTH_URL | Location of the auth server. By default, this is the Cal.com docker instance itself. | optional | `{NEXT_PUBLIC_WEBAPP_URL}/api/auth` |
|
||||
| NEXTAUTH_SECRET | must match build variable | required | `secret` |
|
||||
| CALENDSO_ENCRYPTION_KEY | must match build variable | required | `secret` |
|
||||
|
||||
##### Build-time variables
|
||||
|
||||
If building the image yourself, these variables must be provided at the time of the docker build, and can be provided by updating the .env file. Currently, if you require changes to these variables, you must follow the instructions to build and publish your own image.
|
||||
|
||||
Updating these variables is not required for evaluation, but is required for running in production. Instructions for generating variables can be found in the [Cal.com instructions](https://github.com/calcom/cal.com)
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------------------------------------------------------- |
|
||||
| DATABASE_URL | database url with credentials - if using a connection pooler, this setting should point there | required | `postgresql://unicorn_user:magical_password@database:5432/calendso` |
|
||||
| MAX_OLD_SPACE_SIZE | Needed for Nodejs/NPM build options | required | 4096 |
|
||||
| NEXT_PUBLIC_LICENSE_CONSENT | license consent - true/false | required | |
|
||||
| NEXTAUTH_SECRET | Cookie encryption key | required | `secret` |
|
||||
| CALENDSO_ENCRYPTION_KEY | Authentication encryption key | required | `secret` |
|
||||
| NEXT_PUBLIC_WEBAPP_URL | Base URL injected into static files | optional | `http://localhost:3000` |
|
||||
| NEXT_PUBLIC_WEBSITE_TERMS_URL | custom URL for terms and conditions website | optional | `https://cal.com/terms` |
|
||||
| NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL | custom URL for privacy policy website | optional | `https://cal.com/privacy` |
|
||||
| NEXT_PUBLIC_API_V2_URL | URL for the v2 API, only required for custom integrations or custom booking experiences using [Cal.com Platform](https://cal.com/platform) | optional | |
|
||||
| CALCOM_TELEMETRY_DISABLED | Allow Cal.com to collect anonymous usage data (set to `1` to disable) | optional | |
|
||||
| NEXT_PUBLIC_SINGLE_ORG_SLUG | Required if ORGANIZATIONS_ENABLED is true | optional | |
|
||||
| ORGANIZATIONS_ENABLED | Used for Enterprise or Organizations plan | optional | |
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
##### SSL edge termination
|
||||
|
||||
If running behind a load balancer which handles SSL certificates, you will need to add the environmental variable `NODE_TLS_REJECT_UNAUTHORIZED=0` to prevent requests from being rejected. Only do this if you know what you are doing and trust the services/load-balancers directing traffic to your service.
|
||||
|
||||
##### Failed to commit changes: Invalid 'prisma.user.create()'
|
||||
|
||||
Certain versions may have trouble creating a user if the field `metadata` is empty. Using an empty json object `{}` as the field value should resolve this issue. Also, the `id` field will autoincrement, so you may also try leaving the value of `id` as empty.
|
||||
|
||||
##### CLIENT_FETCH_ERROR
|
||||
|
||||
If you experience this error, it may be the way the default Auth callback in the server is using the WEBAPP_URL as a base url. The container does not necessarily have access to the same DNS as your local machine, and therefor needs to be configured to resolve to itself. You may be able to correct this by configuring `NEXTAUTH_URL=http://localhost:3000/api/auth`, to help the backend loop back to itself.
|
||||
|
||||
```
|
||||
docker-calcom-1 | @calcom/web:start: [next-auth][error][CLIENT_FETCH_ERROR]
|
||||
docker-calcom-1 | @calcom/web:start: https://next-auth.js.org/errors#client_fetch_error request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost {
|
||||
docker-calcom-1 | @calcom/web:start: error: {
|
||||
docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost',
|
||||
docker-calcom-1 | @calcom/web:start: stack: 'FetchError: request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.<anonymous> (/calcom/node_modules/next/dist/compiled/node-fetch/index.js:1:65756)\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.emit (node:events:513:28)\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at ClientRequest.emit (node:domain:489:12)\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at Socket.socketErrorListener (node:_http_client:494:9)\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at Socket.emit (node:events:513:28)\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at Socket.emit (node:domain:489:12)\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at emitErrorNT (node:internal/streams/destroy:157:8)\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at emitErrorCloseNT (node:internal/streams/destroy:122:3)\n' +
|
||||
docker-calcom-1 | @calcom/web:start: ' at processTicksAndRejections (node:internal/process/task_queues:83:21)',
|
||||
docker-calcom-1 | @calcom/web:start: name: 'FetchError'
|
||||
docker-calcom-1 | @calcom/web:start: },
|
||||
docker-calcom-1 | @calcom/web:start: url: 'http://testing.localhost:3000/api/auth/session',
|
||||
docker-calcom-1 | @calcom/web:start: message: 'request to http://testing.localhost:3000/api/auth/session failed, reason: getaddrinfo ENOTFOUND testing.localhost'
|
||||
docker-calcom-1 | @calcom/web:start: }
|
||||
```
|
||||
|
||||
<img referrerpolicy="no-referrer-when-downgrade" src="https://static.scarf.sh/a.png?x-pxid=81cda9f7-a102-453b-ac01-51c35650bd70" />
|
||||
Cal.com, Inc. does not provide official support for Docker, but we will accept fixes and documentation. Use at your own risk.
|
||||
|
||||
### Railway
|
||||
|
||||
@@ -679,7 +425,7 @@ See the [roadmap project](https://cal.com/roadmap) for a list of proposed featur
|
||||
|
||||
Cal.com, Inc. is a commercial open source company, which means some parts of this open source repository require a commercial license. The concept is called "Open Core" where the core technology (99%) is fully open source, licensed under [AGPLv3](https://opensource.org/license/agpl-v3) and the last 1% is covered under a commercial license (["/ee" Enterprise Edition](https://github.com/calcom/cal.com/tree/main/packages/features/ee)) which we believe is entirely relevant for larger organisations that require enterprise features. Enterprise features are built by the core engineering team of Cal.com, Inc. which is hired in full-time. Find their compensation on https://cal.com/open.
|
||||
|
||||
> [!NOTE]
|
||||
> [!NOTE]
|
||||
> Our philosophy is simple, all "Singleplayer APIs" are open-source under AGPLv3. All commercial "Multiplayer APIs" are under a commercial license.
|
||||
|
||||
| | AGPLv3 | EE |
|
||||
@@ -751,9 +497,9 @@ Don't code but still want to contribute? Join our [Discussions](https://github.c
|
||||
- Set CSP_POLICY="non-strict" env variable, which enables [Strict CSP](https://web.dev/strict-csp/) except for unsafe-inline in style-src . If you have some custom changes in your instance, you might have to make some code change to make your instance CSP compatible. Right now it enables strict CSP only on login page and on other SSR pages it is enabled in Report only mode to detect possible issues. On, SSG pages it is still not supported.
|
||||
|
||||
## Single Org Mode
|
||||
|
||||
Refer to docs [here](./docs/self-hosting/guides/organization/single-organization-setup) for a detailed documentation with screenshots.
|
||||
|
||||
|
||||
## Integrations
|
||||
|
||||
### Obtaining the Google API Credentials
|
||||
@@ -836,12 +582,12 @@ following
|
||||
|
||||
1. Open [HubSpot Developer](https://developer.hubspot.com/) and sign into your account, or create a new one.
|
||||
2. From within the home of the Developer account page, go to "Manage apps".
|
||||
3. Click "Create legacy app" button top right and select public app.
|
||||
3. Click "Create app" button top right.
|
||||
4. Fill in any information you want in the "App info" tab
|
||||
5. Go to tab "Auth"
|
||||
6. Now copy the Client ID and Client Secret to your `.env` file into the `HUBSPOT_CLIENT_ID` and `HUBSPOT_CLIENT_SECRET` fields.
|
||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/hubspot/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scopes called `crm.objects.contacts` and `crm.lists`.
|
||||
8. In the "Scopes" section at the bottom of the page, make sure you select "Read" and "Write" for scope called `crm.objects.contacts`
|
||||
9. Click the "Save" button at the bottom footer.
|
||||
10. You're good to go. Now you can see any booking in Cal.com created as a meeting in HubSpot for your contacts.
|
||||
|
||||
@@ -909,7 +655,6 @@ following
|
||||
We use changesets to generate changelogs and publish public packages (packages with `private: true` are ignored).
|
||||
|
||||
An example of good readme is [atoms readme](https://github.com/calcom/cal.com/blob/main/packages/platform/atoms/README.md). Every public package must:
|
||||
|
||||
1. Follow semantic versioning when using changesets.
|
||||
2. Mark breaking changes using `❗️Breaking change`
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export const eventTypeSelect = {
|
||||
},
|
||||
},
|
||||
owner: {
|
||||
select: { username: true, id: true, timeZone: true },
|
||||
select: { username: true, id: true },
|
||||
},
|
||||
children: {
|
||||
select: {
|
||||
|
||||
@@ -16,15 +16,13 @@ export const isAdminGuard = async (req: NextApiRequest) => {
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
role: {
|
||||
in: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
},
|
||||
team: {
|
||||
isOrganization: true,
|
||||
organizationSettings: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
},
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
|
||||
@@ -27,7 +27,7 @@ const getAllAdminMemberships = async (userId: number) => {
|
||||
where: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] },
|
||||
OR: [{ role: MembershipRole.OWNER }, { role: MembershipRole.ADMIN }],
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
@@ -56,19 +56,16 @@ export const getAccessibleUsers = async ({
|
||||
memberUserIds,
|
||||
adminUserId,
|
||||
}: AccessibleUsersType): Promise<number[]> => {
|
||||
const orConditions = [];
|
||||
if (memberUserIds.length > 0) {
|
||||
orConditions.push({ userId: { in: memberUserIds } });
|
||||
}
|
||||
orConditions.push({ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } });
|
||||
|
||||
const memberships = await prisma.membership.findMany({
|
||||
where: {
|
||||
team: {
|
||||
isOrganization: true,
|
||||
},
|
||||
accepted: true,
|
||||
OR: orConditions,
|
||||
OR: [
|
||||
{ userId: { in: memberUserIds } },
|
||||
{ userId: adminUserId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } },
|
||||
],
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
|
||||
@@ -113,12 +113,6 @@ export const schemaBookingReadPublic = Booking.extend({
|
||||
)
|
||||
.optional(),
|
||||
responses: z.record(z.any()).nullable(),
|
||||
// Override metadata to handle reassignment objects from Round Robin/Managed Events
|
||||
// Safe to use z.any() here because:
|
||||
// 1. API v1 POST only accepts z.record(z.string()) for metadata (user input restricted)
|
||||
// 2. API v1 PATCH does not accept metadata changes at all
|
||||
// 3. Complex metadata (objects) are only set by trusted internal features
|
||||
metadata: z.record(z.any()).nullable(),
|
||||
}).pick({
|
||||
id: true,
|
||||
userId: true,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export function middleware(_request: NextRequest) {
|
||||
// Matcher guarantees the last segment is one of the blocked segments, so we can
|
||||
// immediately return a 403 without further path checks.
|
||||
return NextResponse.json({ message: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
export const config = {
|
||||
// The blocked segment is always the last part of the path, so we only need this matcher.
|
||||
matcher: [
|
||||
"/:path*/_get",
|
||||
"/:path*/_post",
|
||||
"/:path*/_patch",
|
||||
"/:path*/_delete",
|
||||
"/:path*/_auth-middleware",
|
||||
],
|
||||
};
|
||||
@@ -35,7 +35,7 @@
|
||||
"@sentry/nextjs": "^9.15.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"memory-cache": "^0.2.0",
|
||||
"next": "15.4.10",
|
||||
"next": "^15.4.5",
|
||||
"next-api-middleware": "^1.0.1",
|
||||
"next-axiom": "^0.17.0",
|
||||
"next-swagger-doc": "^0.3.6",
|
||||
|
||||
@@ -12,7 +12,7 @@ async function authMiddleware(req: NextApiRequest) {
|
||||
if (isSystemWideAdmin) return;
|
||||
// Find all references where the user has bookings
|
||||
const bookingReference = await prisma.bookingReference.findFirst({
|
||||
where: { id, booking: { userId }, deleted: null },
|
||||
where: { id, booking: { userId } },
|
||||
});
|
||||
if (!bookingReference) throw new HttpError({ statusCode: 403, message: "Forbidden" });
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
export async function deleteHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
await prisma.bookingReference.update({ where: { id }, data: { deleted: true } });
|
||||
await prisma.bookingReference.delete({ where: { id } });
|
||||
return { message: `BookingReference with id: ${id} deleted` };
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||
export async function getHandler(req: NextApiRequest) {
|
||||
const { query } = req;
|
||||
const { id } = schemaQueryIdParseInt.parse(query);
|
||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id, deleted: null } });
|
||||
const booking_reference = await prisma.bookingReference.findUniqueOrThrow({ where: { id } });
|
||||
return { booking_reference: schemaBookingReferenceReadPublic.parse(booking_reference) };
|
||||
}
|
||||
|
||||
|
||||
@@ -29,13 +29,13 @@ import { schemaBookingReferenceReadPublic } from "~/lib/validations/booking-refe
|
||||
* 404:
|
||||
* description: No booking references were found
|
||||
*/
|
||||
export async function handler(req: NextApiRequest) {
|
||||
async function getHandler(req: NextApiRequest) {
|
||||
const { userId, isSystemWideAdmin } = req;
|
||||
const args: Prisma.BookingReferenceFindManyArgs = isSystemWideAdmin
|
||||
? { where: { deleted: null } }
|
||||
: { where: { booking: { userId }, deleted: null } };
|
||||
? {}
|
||||
: { where: { booking: { userId } } };
|
||||
const data = await prisma.bookingReference.findMany(args);
|
||||
return { booking_references: data.map((br) => schemaBookingReferenceReadPublic.parse(br)) };
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
export default defaultResponder(getHandler);
|
||||
|
||||
@@ -3,8 +3,8 @@ import utc from "dayjs/plugin/utc";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
|
||||
import { isSupportedTimeZone } from "@calcom/lib/dayjs";
|
||||
import { getAvailableSlotsService } from "@calcom/features/di/containers/AvailableSlots";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { defaultResponder } from "@calcom/lib/server/defaultResponder";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
@@ -48,7 +48,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
: availableSlots.slots;
|
||||
|
||||
return { slots: slotsInProvidedTimeZone };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (cause) {
|
||||
if (cause instanceof TRPCError) {
|
||||
const statusCode = getHTTPStatusCodeFromError(cause);
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Booking, Credential, EventType, User } from "@calcom/prisma/client";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
import { patchHandler } from "../../pages/api/booking-references/[id]/_patch";
|
||||
import { handler } from "../../pages/api/booking-references/_get";
|
||||
|
||||
type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("GET /api/booking-references - Soft-delete filtering", () => {
|
||||
let testUser: User;
|
||||
let testCredential: Credential;
|
||||
let testEventType: EventType;
|
||||
let testBooking: Booking;
|
||||
const createdBookingReferenceIds: number[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "api-bookingreference-test@example.com",
|
||||
username: "api-bookingreference-test",
|
||||
},
|
||||
});
|
||||
|
||||
testCredential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
key: {},
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testEventType = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Test Event Type",
|
||||
slug: "api-test-event-type",
|
||||
length: 30,
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: "api-test-booking-uid",
|
||||
title: "API Test Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: testUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdBookingReferenceIds.length > 0) {
|
||||
await prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: createdBookingReferenceIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
id: testBooking.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.eventType.delete({
|
||||
where: {
|
||||
id: testEventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
id: testCredential.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: testUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should only return active booking references, excluding soft-deleted ones", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `api-active-ref-${timestamp}`,
|
||||
meetingId: `api-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `api-deleted-ref-${timestamp}`,
|
||||
meetingId: `api-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
expect(responseData.booking_references).toBeDefined();
|
||||
expect(Array.isArray(responseData.booking_references)).toBe(true);
|
||||
|
||||
const returnedActiveRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === activeRef.id
|
||||
);
|
||||
const returnedDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === deletedRef.id
|
||||
);
|
||||
|
||||
expect(returnedActiveRef).toBeDefined();
|
||||
expect(returnedDeletedRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should filter booking references by user when not system admin", async () => {
|
||||
const timestamp = Date.now();
|
||||
const otherUser = await prisma.user.create({
|
||||
data: {
|
||||
email: `other-user-bookingreference-${timestamp}@example.com`,
|
||||
username: `other-user-bookingreference-${timestamp}`,
|
||||
},
|
||||
});
|
||||
|
||||
const otherBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: `other-user-booking-uid-${timestamp}`,
|
||||
title: "Other User Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: otherUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
|
||||
const otherUserRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "zoom_video",
|
||||
uid: `other-user-ref-${timestamp}`,
|
||||
meetingId: `other-user-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: otherBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(otherUserRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const userRefs = responseData.booking_references.filter(
|
||||
(ref: { id: number }) => ref.id === otherUserRef.id
|
||||
);
|
||||
|
||||
expect(userRefs).toHaveLength(0);
|
||||
|
||||
await prisma.booking.delete({ where: { id: otherBooking.id } });
|
||||
await prisma.user.delete({ where: { id: otherUser.id } });
|
||||
});
|
||||
|
||||
it("should return only active booking references for user, not soft-deleted ones for system admin", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef1 = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-active-ref-1-${timestamp}`,
|
||||
meetingId: `admin-active-meeting-1-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef1.id);
|
||||
|
||||
const activeRef2 = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-active-ref-2-${timestamp}`,
|
||||
meetingId: `admin-active-meeting-2-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef2.id);
|
||||
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "daily_video",
|
||||
uid: `admin-deleted-ref-${timestamp}`,
|
||||
meetingId: `admin-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const activeRefs = responseData.booking_references.filter(
|
||||
(ref: { id: number }) => ref.id === activeRef1.id || ref.id === activeRef2.id
|
||||
);
|
||||
const returnedDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === deletedRef.id
|
||||
);
|
||||
|
||||
expect(activeRefs.length).toBe(2);
|
||||
expect(returnedDeletedRef).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should verify soft-deleted references are never returned through the API", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "office365_calendar",
|
||||
uid: `never-return-active-${timestamp}`,
|
||||
meetingId: `never-return-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
await prisma.bookingReference.update({
|
||||
where: { id: activeRef.id },
|
||||
data: { deleted: true },
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
const softDeletedRef = responseData.booking_references.find(
|
||||
(ref: { id: number }) => ref.id === activeRef.id
|
||||
);
|
||||
|
||||
expect(softDeletedRef).toBeUndefined();
|
||||
|
||||
const refInDb = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(refInDb).toBeDefined();
|
||||
expect(refInDb?.deleted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PATCH /api/booking-references/[id] - Existing functionality", () => {
|
||||
let testUser: User;
|
||||
let testCredential: Credential;
|
||||
let testEventType: EventType;
|
||||
let testBooking: Booking;
|
||||
const createdBookingReferenceIds: number[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
testUser = await prisma.user.create({
|
||||
data: {
|
||||
email: "api-bookingreference-patch-test@example.com",
|
||||
username: "api-bookingreference-patch-test",
|
||||
},
|
||||
});
|
||||
|
||||
testCredential = await prisma.credential.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
key: {},
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testEventType = await prisma.eventType.create({
|
||||
data: {
|
||||
title: "Test Event Type",
|
||||
slug: "api-patch-test-event-type",
|
||||
length: 30,
|
||||
userId: testUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
testBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: "api-patch-test-booking-uid",
|
||||
title: "API Patch Test Booking",
|
||||
startTime: new Date(Date.now() + 86400000),
|
||||
endTime: new Date(Date.now() + 90000000),
|
||||
userId: testUser.id,
|
||||
eventTypeId: testEventType.id,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdBookingReferenceIds.length > 0) {
|
||||
await prisma.bookingReference.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: createdBookingReferenceIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
id: testBooking.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.eventType.delete({
|
||||
where: {
|
||||
id: testEventType.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.credential.delete({
|
||||
where: {
|
||||
id: testCredential.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.delete({
|
||||
where: {
|
||||
id: testUser.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully update an active booking reference", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `patch-active-ref-${timestamp}`,
|
||||
meetingId: `patch-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: activeRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "new-password-123",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.id).toBe(activeRef.id);
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("new-password-123");
|
||||
|
||||
const updatedRef = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(updatedRef?.meetingPassword).toBe("new-password-123");
|
||||
});
|
||||
|
||||
it("should allow updating a soft-deleted booking reference (existing functionality)", async () => {
|
||||
const timestamp = Date.now();
|
||||
const deletedRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "google_calendar",
|
||||
uid: `patch-deleted-ref-${timestamp}`,
|
||||
meetingId: `patch-deleted-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
deleted: true,
|
||||
meetingPassword: "original-password",
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(deletedRef.id);
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: deletedRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "updated-password",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.id).toBe(deletedRef.id);
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
|
||||
|
||||
const updatedRef = await prisma.bookingReference.findUnique({
|
||||
where: { id: deletedRef.id },
|
||||
});
|
||||
expect(updatedRef?.meetingPassword).toBe("updated-password");
|
||||
expect(updatedRef?.deleted).toBe(true);
|
||||
});
|
||||
|
||||
it("should verify that booking references can be updated regardless of deleted status", async () => {
|
||||
const timestamp = Date.now();
|
||||
const activeRef = await prisma.bookingReference.create({
|
||||
data: {
|
||||
type: "zoom_video",
|
||||
uid: `patch-verify-active-${timestamp}`,
|
||||
meetingId: `patch-verify-active-meeting-${timestamp}`,
|
||||
credentialId: testCredential.id,
|
||||
bookingId: testBooking.id,
|
||||
meetingPassword: "original-password",
|
||||
},
|
||||
});
|
||||
createdBookingReferenceIds.push(activeRef.id);
|
||||
|
||||
await prisma.bookingReference.update({
|
||||
where: { id: activeRef.id },
|
||||
data: { deleted: true },
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
query: { id: activeRef.id.toString() },
|
||||
body: {
|
||||
meetingPassword: "updated-password",
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = testUser.id;
|
||||
req.isSystemWideAdmin = false;
|
||||
|
||||
const responseData = await patchHandler(req);
|
||||
|
||||
expect(responseData.booking_reference).toBeDefined();
|
||||
expect(responseData.booking_reference.meetingPassword).toBe("updated-password");
|
||||
|
||||
const refInDb = await prisma.bookingReference.findUnique({
|
||||
where: { id: activeRef.id },
|
||||
});
|
||||
expect(refInDb?.meetingPassword).toBe("updated-password");
|
||||
expect(refInDb?.deleted).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -11,114 +11,45 @@ type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("PATCH /api/bookings", () => {
|
||||
let member1Booking: Awaited<ReturnType<typeof prisma.booking.create>>;
|
||||
let member0Booking: Awaited<ReturnType<typeof prisma.booking.create>>;
|
||||
const createdBookingIds: number[] = [];
|
||||
let testAdminUserId: number | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
const member1 = await prisma.user.findFirstOrThrow({
|
||||
where: { email: "member1-acme@example.com" },
|
||||
});
|
||||
|
||||
const member0 = await prisma.user.findFirstOrThrow({
|
||||
where: { email: "member0-acme@example.com" },
|
||||
});
|
||||
|
||||
// Create bookings for testing
|
||||
member1Booking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: `test-member1-booking-${Date.now()}`,
|
||||
title: "Member 1 Test Booking",
|
||||
startTime: new Date(Date.now() + 86400000), // Tomorrow
|
||||
endTime: new Date(Date.now() + 90000000), // Tomorrow + 1 hour
|
||||
userId: member1.id,
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
});
|
||||
createdBookingIds.push(member1Booking.id);
|
||||
|
||||
member0Booking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: `test-member0-booking-${Date.now()}`,
|
||||
title: "Member 0 Test Booking",
|
||||
startTime: new Date(Date.now() + 172800000), // Day after tomorrow
|
||||
endTime: new Date(Date.now() + 176400000), // Day after tomorrow + 1 hour
|
||||
userId: member0.id,
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
});
|
||||
createdBookingIds.push(member0Booking.id);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (createdBookingIds.length > 0) {
|
||||
await prisma.booking.deleteMany({
|
||||
where: { id: { in: createdBookingIds } },
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up test admin user if created
|
||||
if (testAdminUserId) {
|
||||
await prisma.user.delete({
|
||||
where: { id: testAdminUserId },
|
||||
});
|
||||
}
|
||||
});
|
||||
it("Returns 403 when user has no permission to the booking", async () => {
|
||||
// Member2 tries to access Member0's booking - should fail
|
||||
const member2 = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
||||
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
body: {
|
||||
title: member0Booking.title,
|
||||
startTime: member0Booking.startTime.toISOString(),
|
||||
endTime: member0Booking.endTime.toISOString(),
|
||||
userId: member2.id,
|
||||
title: booking.title,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
userId: memberUser.id,
|
||||
},
|
||||
query: {
|
||||
id: member0Booking.id,
|
||||
id: booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
req.userId = member2.id;
|
||||
req.userId = memberUser.id;
|
||||
|
||||
await handler(req, res);
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it("Allows PATCH when user is system-wide admin", async () => {
|
||||
// Check if admin user already exists before upserting
|
||||
const existingAdmin = await prisma.user.findUnique({ where: { email: "test-admin@example.com" } });
|
||||
|
||||
// Create a system-wide admin user for this test
|
||||
const adminUser = await prisma.user.upsert({
|
||||
where: { email: "test-admin@example.com" },
|
||||
update: { role: "ADMIN" },
|
||||
create: {
|
||||
email: "test-admin@example.com",
|
||||
username: "test-admin",
|
||||
name: "Test Admin",
|
||||
role: "ADMIN",
|
||||
},
|
||||
});
|
||||
|
||||
// Only track for cleanup if we created it (not if it already existed)
|
||||
if (!existingAdmin) {
|
||||
testAdminUserId = adminUser.id;
|
||||
}
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "admin@example.com" } });
|
||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
||||
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
body: {
|
||||
title: member0Booking.title,
|
||||
startTime: member0Booking.startTime.toISOString(),
|
||||
endTime: member0Booking.endTime.toISOString(),
|
||||
userId: member0Booking.userId,
|
||||
title: booking.title,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
userId: proUser.id,
|
||||
},
|
||||
query: {
|
||||
id: member0Booking.id,
|
||||
id: booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -131,17 +62,19 @@ describe("PATCH /api/bookings", () => {
|
||||
|
||||
it("Allows PATCH when user is org-wide admin", async () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member1-acme@example.com" } });
|
||||
const booking = await prisma.booking.findFirstOrThrow({ where: { userId: memberUser.id } });
|
||||
|
||||
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "PATCH",
|
||||
body: {
|
||||
title: member1Booking.title,
|
||||
startTime: member1Booking.startTime.toISOString(),
|
||||
endTime: member1Booking.endTime.toISOString(),
|
||||
userId: member1Booking.userId,
|
||||
title: booking.title,
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
userId: memberUser.id,
|
||||
},
|
||||
query: {
|
||||
id: member1Booking.id,
|
||||
id: booking.id,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import {
|
||||
getRecordingsOfCalVideoByRoomName,
|
||||
getDownloadLinkOfCalVideoByRecordingId,
|
||||
} from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, test, vi, afterEach } from "vitest";
|
||||
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
|
||||
import { buildBooking } from "@calcom/lib/test/builder";
|
||||
import { getAllTranscriptsAccessLinkFromRoomName } from "@calcom/features/conferencing/lib/videoClient";
|
||||
|
||||
import { getAccessibleUsers } from "~/lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, expect, it, beforeAll, afterAll } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { handler } from "../../../pages/api/bookings/_get";
|
||||
|
||||
@@ -16,67 +16,27 @@ const DefaultPagination = {
|
||||
skip: 0,
|
||||
};
|
||||
|
||||
describe("GET /api/bookings", () => {
|
||||
let proUser: Awaited<ReturnType<typeof prisma.user.findFirstOrThrow>>;
|
||||
let proUserBooking: Awaited<ReturnType<typeof prisma.booking.findFirstOrThrow>>;
|
||||
let memberUser: Awaited<ReturnType<typeof prisma.user.findFirstOrThrow>>;
|
||||
let memberUserBooking: Awaited<ReturnType<typeof prisma.booking.create>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
||||
|
||||
memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||
|
||||
// Find an event type for memberUser or use a simple booking
|
||||
const memberEventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ userId: memberUser.id },
|
||||
{ team: { members: { some: { userId: memberUser.id } } } }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
memberUserBooking = await prisma.booking.create({
|
||||
data: {
|
||||
uid: `test-member-booking-${Date.now()}`,
|
||||
title: "Member Test Booking",
|
||||
startTime: new Date(Date.now() + 86400000), // Tomorrow
|
||||
endTime: new Date(Date.now() + 90000000), // Tomorrow + 1 hour
|
||||
userId: memberUser.id,
|
||||
eventTypeId: memberEventType?.id,
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Clean up the test booking created in beforeAll
|
||||
if (memberUserBooking?.id) {
|
||||
await prisma.booking.delete({
|
||||
where: { id: memberUserBooking.id },
|
||||
});
|
||||
}
|
||||
});
|
||||
describe("GET /api/bookings", async () => {
|
||||
const proUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
const proUserBooking = await prisma.booking.findFirstOrThrow({ where: { userId: proUser.id } });
|
||||
|
||||
it("Does not return bookings of other users when user has no permission", async () => {
|
||||
const memberUser = await prisma.user.findFirstOrThrow({ where: { email: "member2-acme@example.com" } });
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {
|
||||
userId: proUser.id, // Try to access proUser's bookings
|
||||
userId: proUser.id,
|
||||
},
|
||||
pagination: DefaultPagination,
|
||||
});
|
||||
|
||||
req.userId = memberUser.id; // But request is from memberUser
|
||||
req.userId = memberUser.id;
|
||||
|
||||
const responseData = await handler(req);
|
||||
const groupedUsers = new Set(responseData.bookings.map((b) => b.userId));
|
||||
|
||||
// Should only return memberUser's own bookings, not proUser's
|
||||
expect(responseData.bookings.find((b) => b.userId === memberUser.id)).toBeDefined();
|
||||
expect(responseData.bookings.find((b) => b.id === memberUserBooking.id)).toBeDefined();
|
||||
expect(groupedUsers.size).toBe(1);
|
||||
const firstEntry = groupedUsers.entries().next().value;
|
||||
expect(firstEntry?.[0]).toBe(memberUser.id);
|
||||
@@ -233,22 +193,6 @@ describe("GET /api/bookings", () => {
|
||||
describe("Expand feature to add relational data in return payload", () => {
|
||||
it("Returns only team data when expand=team is set", async () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
|
||||
// Find a team booking and a non-team booking from seed data
|
||||
const team1 = await prisma.team.findFirst({ where: { slug: "team1" } });
|
||||
const teamEventType = await prisma.eventType.findFirst({
|
||||
where: { teamId: team1?.id },
|
||||
include: { bookings: true },
|
||||
});
|
||||
const teamBooking = teamEventType?.bookings[0];
|
||||
|
||||
const nonTeamBooking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
eventType: { teamId: null },
|
||||
userId: proUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {
|
||||
@@ -261,23 +205,14 @@ describe("GET /api/bookings", () => {
|
||||
req.isOrganizationOwnerOrAdmin = true;
|
||||
|
||||
const responseData = await handler(req);
|
||||
|
||||
// Verify team booking has team data
|
||||
if (teamBooking) {
|
||||
const returnedTeamBooking = responseData.bookings.find((b) => b.id === teamBooking.id);
|
||||
expect(returnedTeamBooking?.eventType?.team?.slug).toBe("team1");
|
||||
}
|
||||
|
||||
// Verify non-team booking has null/undefined team
|
||||
if (nonTeamBooking) {
|
||||
const returnedNonTeamBooking = responseData.bookings.find((b) => b.id === nonTeamBooking.id);
|
||||
if (returnedNonTeamBooking) {
|
||||
expect(
|
||||
returnedNonTeamBooking.eventType?.team === null ||
|
||||
returnedNonTeamBooking.eventType?.team === undefined
|
||||
).toBe(true);
|
||||
console.log("bookings=>", responseData.bookings);
|
||||
responseData.bookings.forEach((booking) => {
|
||||
if (booking.id === 31) expect(booking.eventType?.team?.slug).toBe("team1");
|
||||
if (booking.id === 19) {
|
||||
// The team field can be either null or undefined due to nullish() in the schema
|
||||
expect(booking.eventType?.team === null || booking.eventType?.team === undefined).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -383,6 +318,11 @@ describe("GET /api/bookings", () => {
|
||||
|
||||
const testUser = await prisma.user.findFirstOrThrow({ where: { email: "pro@example.com" } });
|
||||
|
||||
const testUserBooking = await prisma.booking.findFirstOrThrow({
|
||||
where: { userId: testUser.id },
|
||||
include: { attendees: true },
|
||||
});
|
||||
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "GET",
|
||||
query: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -12,53 +12,6 @@ type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("isAdmin guard", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const dunderOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "dunder-mifflin",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (dunderOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: dunderOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: false,
|
||||
},
|
||||
create: {
|
||||
organizationId: dunderOrg.id,
|
||||
orgAutoAcceptEmail: "dunder-mifflin.com",
|
||||
isAdminAPIEnabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
it("Returns false when user does not exist in the system", async () => {
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
@@ -8,30 +8,6 @@ import {
|
||||
} from "../../../lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
describe("retrieveScopedAccessibleUsers tests", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
describe("getAccessibleUsers", () => {
|
||||
it("Does not return members when only admin user ID is supplied", async () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
|
||||
@@ -41,11 +41,4 @@ LOGGER_BRIDGE_LOG_LEVEL="1"
|
||||
|
||||
# 1: Rewrite /api/v2 to /v2
|
||||
# 0: Don't rewrite
|
||||
REWRITE_API_V2_PREFIX="1"
|
||||
|
||||
|
||||
# Trigger.dev
|
||||
ENABLE_ASYNC_TASKER="false" # set to "true" to enable
|
||||
TRIGGER_SECRET_KEY=
|
||||
TRIGGER_API_URL=https://api.trigger.dev
|
||||
TRIGGER_DEV_PROJECT_REF=
|
||||
REWRITE_API_V2_PREFIX="1"
|
||||
@@ -42,5 +42,4 @@ lerna-debug.log*
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.appStore.example
|
||||
/client/*
|
||||
/generated/prisma/*
|
||||
/client/*
|
||||
@@ -16,8 +16,14 @@ ENV DATABASE_URL=${DATABASE_URL}
|
||||
COPY . .
|
||||
|
||||
RUN yarn install
|
||||
|
||||
# Build prisma schema and make sure that it is linked to v2 node_modules
|
||||
RUN yarn workspace @calcom/api-v2 run generate-schemas
|
||||
RUN rm -rf apps/api/v2/node_modules
|
||||
RUN yarn install
|
||||
|
||||
RUN yarn workspace @calcom/platform-libraries run build
|
||||
|
||||
RUN yarn workspace @calcom/api-v2 run build
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
@@ -22,10 +22,6 @@ const loadConfig = (): AppConfig => {
|
||||
db: {
|
||||
readUrl: getEnv("DATABASE_READ_URL"),
|
||||
writeUrl: getEnv("DATABASE_WRITE_URL"),
|
||||
readPoolMax: getEnv("DATABASE_READ_POOL_MAX", 9),
|
||||
writePoolMax: getEnv("DATABASE_WRITE_POOL_MAX", 7),
|
||||
workerReadPoolMax: getEnv("DATABASE_READ_WORKER_POOL_MAX", 4),
|
||||
workerWritePoolMax: getEnv("DATABASE_WRITE_WORKER_POOL_MAX", 6),
|
||||
redisUrl: getEnv("REDIS_URL"),
|
||||
},
|
||||
next: {
|
||||
|
||||
@@ -13,10 +13,6 @@ export type AppConfig = {
|
||||
db: {
|
||||
readUrl: string;
|
||||
writeUrl: string;
|
||||
readPoolMax: number;
|
||||
writePoolMax: number;
|
||||
workerReadPoolMax: number;
|
||||
workerWritePoolMax: number;
|
||||
redisUrl: string;
|
||||
};
|
||||
next: {
|
||||
|
||||
@@ -9,8 +9,6 @@ import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/
|
||||
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
|
||||
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
|
||||
import { RegularBookingModule } from "@/lib/modules/regular-booking.module";
|
||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
@@ -57,8 +55,6 @@ import { Module } from "@nestjs/common";
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
PrismaEventTypeRepository,
|
||||
PrismaTeamRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_04_15],
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
|
||||
import { CreateRecurringBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-recurring-booking.input";
|
||||
import { GetBookingOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-booking.output";
|
||||
import { GetBookingsOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/get-bookings.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
@@ -22,7 +21,6 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { BookingResponse } from "@calcom/platform-libraries";
|
||||
import { type RegularBookingCreateResult } from "@calcom/platform-libraries/bookings";
|
||||
import type { ApiSuccessResponse, ApiErrorResponse } from "@calcom/platform-types";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
@@ -42,7 +40,6 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
let user: User;
|
||||
|
||||
let eventTypeId: number;
|
||||
let recEventTypeId: number;
|
||||
|
||||
let createdBooking: RegularBookingCreateResult;
|
||||
|
||||
@@ -84,18 +81,6 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
const recEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `rec-bookings-2024-04-15-event-type-${randomString()}-${describe.name}`,
|
||||
slug: `rec-bookings-2024-04-15-event-type-${randomString()}-${describe.name}`,
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: 4, interval: 1 },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
recEventTypeId = recEventType.id;
|
||||
|
||||
eventTypeId = event.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
@@ -419,210 +404,6 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create recurring bookings", async () => {
|
||||
const bookingEventTypeId = recEventTypeId;
|
||||
const bookingTimeZone = "Europe/London";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingMetadata = {
|
||||
timeFormat: "12",
|
||||
meetingType: "organizer-phone",
|
||||
};
|
||||
const bookingResponses = {
|
||||
name: "tester",
|
||||
email: "tester@example.com",
|
||||
location: {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
},
|
||||
notes: "test",
|
||||
};
|
||||
|
||||
const body: CreateRecurringBookingInput_2024_04_15[] = [
|
||||
{
|
||||
start: "2040-06-21T09:30:00.000Z",
|
||||
end: "2040-06-21T10:30:00.000Z",
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
recurringEventId: "test-recurring-event-id",
|
||||
},
|
||||
{
|
||||
start: "2040-06-27T09:30:00.000Z",
|
||||
end: "2040-06-27T10:30:00.000Z",
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
recurringEventId: "test-recurring-event-id-2",
|
||||
},
|
||||
{
|
||||
start: "2040-06-04T09:30:00.000Z",
|
||||
end: "2040-06-04T10:30:00.000Z",
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
recurringEventId: "test-recurring-event-id-3",
|
||||
},
|
||||
{
|
||||
start: "2040-06-11T09:30:00.000Z",
|
||||
end: "2040-06-11T10:30:00.000Z",
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
recurringEventId: "test-recurring-event-id-4",
|
||||
},
|
||||
];
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings/recurring")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<BookingResponse[]> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
responseBody.data.forEach((booking) => {
|
||||
expect(booking.userPrimaryEmail).toBeDefined();
|
||||
expect(booking.userPrimaryEmail).toEqual(userEmail);
|
||||
expect(booking.id).toBeDefined();
|
||||
expect(booking.uid).toBeDefined();
|
||||
expect(booking.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(booking.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(booking.metadata).toEqual(bookingMetadata);
|
||||
expect(booking.responses).toEqual(bookingResponses);
|
||||
expect(booking.creationSource).toEqual("API_V2");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("event type booking requires authentication", () => {
|
||||
let eventTypeRequiringAuthenticationId: number;
|
||||
let unauthorizedUser: User;
|
||||
let unauthorizedUserApiKeyString: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const eventTypeRequiringAuthentication = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `event-type-requiring-authentication-${randomString()}`,
|
||||
slug: `event-type-requiring-authentication-${randomString()}`,
|
||||
length: 60,
|
||||
requiresConfirmation: true,
|
||||
bookingRequiresAuthentication: true,
|
||||
},
|
||||
user.id
|
||||
);
|
||||
eventTypeRequiringAuthenticationId = eventTypeRequiringAuthentication.id;
|
||||
|
||||
const unauthorizedUserEmail = `unauthorized-user-${randomString()}@api.com`;
|
||||
unauthorizedUser = await userRepositoryFixture.create({
|
||||
email: unauthorizedUserEmail,
|
||||
});
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
|
||||
unauthorizedUserApiKeyString = keyString;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (unauthorizedUser) {
|
||||
await userRepositoryFixture.deleteByEmail(unauthorizedUser.email);
|
||||
}
|
||||
});
|
||||
|
||||
it("can't be booked without credentials", async () => {
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
start: "2040-05-23T09:30:00.000Z",
|
||||
end: "2040-05-23T10:30:00.000Z",
|
||||
eventTypeId: eventTypeRequiringAuthenticationId,
|
||||
timeZone: "Europe/London",
|
||||
language: "en",
|
||||
metadata: {},
|
||||
hashedLink: "",
|
||||
responses: {
|
||||
name: "External Attendee",
|
||||
email: "external@example.com",
|
||||
location: {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer()).post("/v2/bookings").send(body).expect(401);
|
||||
});
|
||||
|
||||
it("can't be booked with unauthorized user credentials", async () => {
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
start: "2040-05-23T10:30:00.000Z",
|
||||
end: "2040-05-23T11:30:00.000Z",
|
||||
eventTypeId: eventTypeRequiringAuthenticationId,
|
||||
timeZone: "Europe/London",
|
||||
language: "en",
|
||||
metadata: {},
|
||||
hashedLink: "",
|
||||
responses: {
|
||||
name: "External Attendee",
|
||||
email: "external@example.com",
|
||||
location: {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set({ Authorization: `Bearer cal_test_${unauthorizedUserApiKeyString}` })
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("can be booked with event type owner credentials", async () => {
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
start: "2040-05-23T11:30:00.000Z",
|
||||
end: "2040-05-23T12:30:00.000Z",
|
||||
eventTypeId: eventTypeRequiringAuthenticationId,
|
||||
timeZone: "Europe/London",
|
||||
language: "en",
|
||||
metadata: {},
|
||||
hashedLink: "",
|
||||
responses: {
|
||||
name: "External Attendee",
|
||||
email: "external@example.com",
|
||||
location: {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||
.expect(201);
|
||||
|
||||
const responseBody: ApiSuccessResponse<RegularBookingCreateResult> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
|
||||
if (responseBody.data.id) {
|
||||
await bookingsRepositoryFixture.deleteById(responseBody.data.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
|
||||
@@ -7,20 +7,13 @@ import { MarkNoShowOutput_2024_04_15 } from "@/ee/bookings/2024-04-15/outputs/ma
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { sha256Hash, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { VERSION_2024_04_15, VERSION_2024_06_11, VERSION_2024_06_14 } from "@/lib/api-versions";
|
||||
import { PrismaEventTypeRepository } from "@/lib/repositories/prisma-event-type.repository";
|
||||
import { PrismaTeamRepository } from "@/lib/repositories/prisma-team.repository";
|
||||
import { InstantBookingCreateService } from "@/lib/services/instant-booking-create.service";
|
||||
import { RecurringBookingService } from "@/lib/services/recurring-booking.service";
|
||||
import { RegularBookingService } from "@/lib/services/regular-booking.service";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import {
|
||||
AuthOptionalUser,
|
||||
GetOptionalUser,
|
||||
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { KyselyReadService } from "@/modules/kysely/kysely-read.service";
|
||||
@@ -45,8 +38,6 @@ import {
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
ForbiddenException,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiQuery, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
@@ -122,10 +113,8 @@ export class BookingsController_2024_04_15 {
|
||||
private readonly usersService: UsersService,
|
||||
private readonly regularBookingService: RegularBookingService,
|
||||
private readonly recurringBookingService: RecurringBookingService,
|
||||
private readonly instantBookingCreateService: InstantBookingCreateService,
|
||||
private readonly eventTypeRepository: PrismaEventTypeRepository,
|
||||
private readonly teamRepository: PrismaTeamRepository
|
||||
) { }
|
||||
private readonly instantBookingCreateService: InstantBookingCreateService
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@@ -177,12 +166,8 @@ export class BookingsController_2024_04_15 {
|
||||
}
|
||||
|
||||
@Get("/:bookingUid/reschedule")
|
||||
@UseGuards(OptionalApiAuthGuard)
|
||||
async getBookingForReschedule(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetOptionalUser() user: AuthOptionalUser
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const booking = await getBookingForReschedule(bookingUid, user?.id);
|
||||
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
const booking = await getBookingForReschedule(bookingUid);
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||
@@ -205,8 +190,6 @@ export class BookingsController_2024_04_15 {
|
||||
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
|
||||
const { orgSlug, locationUrl } = body;
|
||||
try {
|
||||
await this.checkBookingRequiresAuthentication(req, body.eventTypeId, body.rescheduleUid);
|
||||
|
||||
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl, isEmbed);
|
||||
const booking = await this.regularBookingService.createBooking({
|
||||
bookingData: bookingRequest.body,
|
||||
@@ -270,7 +253,6 @@ export class BookingsController_2024_04_15 {
|
||||
const res = await handleCancelBooking({
|
||||
bookingData: bookingRequest.body,
|
||||
userId: bookingRequest.userId,
|
||||
userUuid: bookingRequest.userUuid,
|
||||
arePlatformEmailsEnabled: bookingRequest.arePlatformEmailsEnabled,
|
||||
platformClientId: bookingRequest.platformClientId,
|
||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||
@@ -301,7 +283,7 @@ export class BookingsController_2024_04_15 {
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard)
|
||||
async markNoShow(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@GetUser("id") userId: number,
|
||||
@Body() body: MarkNoShowInput_2024_04_15,
|
||||
@Param("bookingUid") bookingUid: string
|
||||
): Promise<MarkNoShowOutput_2024_04_15> {
|
||||
@@ -310,8 +292,7 @@ export class BookingsController_2024_04_15 {
|
||||
bookingUid: bookingUid,
|
||||
attendees: body.attendees,
|
||||
noShowHost: body.noShowHost,
|
||||
userId: user.id,
|
||||
userUuid: user.uuid,
|
||||
userId,
|
||||
});
|
||||
|
||||
return { status: SUCCESS_STATUS, data: markNoShowResponse };
|
||||
@@ -337,9 +318,11 @@ export class BookingsController_2024_04_15 {
|
||||
recurringEvent.recurringEventId = recurringEventId;
|
||||
}
|
||||
}
|
||||
|
||||
const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
||||
|
||||
const createdBookings: BookingResponse[] = await this.recurringBookingService.createBooking({
|
||||
bookingData: body.map((booking) => ({ ...booking, creationSource: CreationSource.API_V2 })),
|
||||
bookingData: bookingRequest.body,
|
||||
bookingMeta: {
|
||||
userId: bookingRequest.userId,
|
||||
hostname: bookingRequest.headers?.host || "",
|
||||
@@ -348,7 +331,6 @@ export class BookingsController_2024_04_15 {
|
||||
platformCancelUrl: bookingRequest.platformCancelUrl,
|
||||
platformBookingUrl: bookingRequest.platformBookingUrl,
|
||||
platformBookingLocation: bookingRequest.platformBookingLocation,
|
||||
noEmail: bookingRequest.body.noEmail,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -380,7 +362,7 @@ export class BookingsController_2024_04_15 {
|
||||
): Promise<ApiResponse<InstantBookingCreateResult>> {
|
||||
const oAuthClientId =
|
||||
clientId?.toString() || (await this.getOAuthClientIdFromEventType(body.eventTypeId));
|
||||
req.userId = (await this.getOwner(req))?.id ?? -1;
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
try {
|
||||
const bookingReq = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed);
|
||||
const instantMeeting = await this.instantBookingCreateService.createBooking({
|
||||
@@ -407,45 +389,30 @@ export class BookingsController_2024_04_15 {
|
||||
throw new InternalServerErrorException("Could not create instant booking.");
|
||||
}
|
||||
|
||||
private async getOwner(req: Request): Promise<{ id: number; uuid: string } | null> {
|
||||
private async getOwnerId(req: Request): Promise<number | undefined> {
|
||||
try {
|
||||
const bearerToken = req.get("Authorization")?.replace("Bearer ", "");
|
||||
if (!bearerToken) {
|
||||
return null;
|
||||
if (bearerToken) {
|
||||
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
|
||||
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
|
||||
const apiKeyHash = sha256Hash(strippedApiKey);
|
||||
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
||||
return keyData?.userId;
|
||||
} else {
|
||||
// Access Token
|
||||
const ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
return ownerId;
|
||||
}
|
||||
}
|
||||
|
||||
let ownerId: number | null = null;
|
||||
|
||||
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
|
||||
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
|
||||
const apiKeyHash = sha256Hash(strippedApiKey);
|
||||
const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash);
|
||||
ownerId = keyData?.userId ?? null;
|
||||
} else {
|
||||
// Access Token
|
||||
ownerId = await this.oAuthFlowService.getOwnerId(bearerToken);
|
||||
}
|
||||
|
||||
if (!ownerId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findById(ownerId);
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { id: user.id, uuid: user.uuid };
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getOwnerRescheduledBooking(
|
||||
private async getOwnerIdRescheduledBooking(
|
||||
request: Request,
|
||||
platformClientId?: string
|
||||
): Promise<{ id: number; uuid: string } | null> {
|
||||
): Promise<number | undefined> {
|
||||
if (
|
||||
platformClientId &&
|
||||
request.body.rescheduledBy &&
|
||||
@@ -457,15 +424,13 @@ export class BookingsController_2024_04_15 {
|
||||
);
|
||||
}
|
||||
|
||||
if (request.body.rescheduledBy && request.body.rescheduledBy !== request.body.responses.email) {
|
||||
const user = await this.usersRepository.findByEmail(request.body.rescheduledBy);
|
||||
if (!user) {
|
||||
return null;
|
||||
if (request.body.rescheduledBy) {
|
||||
if (request.body.rescheduledBy !== request.body.responses.email) {
|
||||
return (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id;
|
||||
}
|
||||
return { id: user.id, uuid: user.uuid };
|
||||
}
|
||||
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getOAuthClientIdFromEventType(eventTypeId: number): Promise<string | undefined> {
|
||||
@@ -479,77 +444,6 @@ export class BookingsController_2024_04_15 {
|
||||
return oAuthClientParams.platformClientId;
|
||||
}
|
||||
|
||||
private async isValidRescheduleBooking(rescheduleUid: string, eventTypeId: number): Promise<boolean> {
|
||||
const { bookingInfo } = await getBookingInfo(rescheduleUid);
|
||||
if (!bookingInfo) {
|
||||
return false;
|
||||
}
|
||||
if (bookingInfo.status !== "ACCEPTED" && bookingInfo.status !== "PENDING") {
|
||||
return false;
|
||||
}
|
||||
if (bookingInfo.eventTypeId !== eventTypeId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private async checkBookingRequiresAuthentication(
|
||||
req: Request,
|
||||
eventTypeId: number,
|
||||
rescheduleUid?: string
|
||||
): Promise<void> {
|
||||
const eventType = await this.eventTypeRepository.findByIdIncludeHostsAndTeamMembers({
|
||||
id: eventTypeId,
|
||||
});
|
||||
|
||||
if (!eventType?.bookingRequiresAuthentication) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rescheduleUid) {
|
||||
const isValidRescheduleBooking = await this.isValidRescheduleBooking(rescheduleUid, eventTypeId);
|
||||
if (isValidRescheduleBooking) {
|
||||
return;
|
||||
} else {
|
||||
throw new BadRequestException(
|
||||
"Trying to reschedule an event-type which requires authentication but provided invalid rescheduleUid."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const owner = await this.getOwner(req);
|
||||
const userId = owner?.id;
|
||||
|
||||
if (!userId) {
|
||||
throw new UnauthorizedException(
|
||||
"This event type requires authentication. Please provide valid credentials."
|
||||
);
|
||||
}
|
||||
|
||||
const isEventTypeOwner = eventType.userId === userId;
|
||||
const isHost = eventType.hosts.some((host) => host.userId === userId);
|
||||
const isTeamAdminOrOwner = eventType.team?.members.some((member) => member.userId === userId) ?? false;
|
||||
|
||||
let isOrgAdminOrOwner = false;
|
||||
if (eventType.team?.parentId) {
|
||||
const orgTeam = await this.teamRepository.getTeamByIdIfUserIsAdmin({
|
||||
userId,
|
||||
teamId: eventType.team.parentId,
|
||||
});
|
||||
isOrgAdminOrOwner = !!orgTeam;
|
||||
} else if (eventType.team?.isOrganization) {
|
||||
isOrgAdminOrOwner = isTeamAdminOrOwner;
|
||||
}
|
||||
|
||||
const isAuthorized = isEventTypeOwner || isHost || isTeamAdminOrOwner || isOrgAdminOrOwner;
|
||||
|
||||
if (!isAuthorized) {
|
||||
throw new ForbiddenException(
|
||||
"You are not authorized to book this event type. You must be the event type owner, a host, a team admin/owner, or an organization admin/owner."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOAuthClientsParams(clientId: string, isEmbed = false): Promise<OAuthRequestParams> {
|
||||
const res = { ...DEFAULT_PLATFORM_PARAMS };
|
||||
|
||||
@@ -581,16 +475,12 @@ export class BookingsController_2024_04_15 {
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string,
|
||||
isEmbed?: string
|
||||
): Promise<NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams> {
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const requestId = req.get("X-Request-Id");
|
||||
const clone = { ...req };
|
||||
const owner = clone.body.rescheduleUid
|
||||
? await this.getOwnerRescheduledBooking(req, oAuthClientId)
|
||||
: await this.getOwner(req);
|
||||
|
||||
const userId = owner?.id;
|
||||
const userUuid = owner?.uuid;
|
||||
|
||||
const userId = clone.body.rescheduleUid
|
||||
? await this.getOwnerIdRescheduledBooking(req, oAuthClientId)
|
||||
: await this.getOwnerId(req);
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
@@ -601,7 +491,7 @@ export class BookingsController_2024_04_15 {
|
||||
oAuthClientId,
|
||||
...oAuthParams,
|
||||
});
|
||||
Object.assign(clone, { userId, userUuid, ...oAuthParams, platformBookingLocation });
|
||||
Object.assign(clone, { userId, ...oAuthParams, platformBookingLocation });
|
||||
clone.body = {
|
||||
...clone.body,
|
||||
noEmail: !oAuthParams.arePlatformEmailsEnabled,
|
||||
@@ -610,13 +500,10 @@ export class BookingsController_2024_04_15 {
|
||||
if (oAuthClientId) {
|
||||
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
|
||||
}
|
||||
return clone as unknown as NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams;
|
||||
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
async setPlatformAttendeesEmails(
|
||||
requestBody: { responses?: { email?: string; guests?: string[] } },
|
||||
oAuthClientId: string
|
||||
): Promise<void> {
|
||||
async setPlatformAttendeesEmails(requestBody: any, oAuthClientId: string): Promise<void> {
|
||||
if (requestBody?.responses?.email) {
|
||||
requestBody.responses.email = await this.platformBookingsService.getPlatformAttendeeEmail(
|
||||
requestBody.responses.email,
|
||||
@@ -636,12 +523,9 @@ export class BookingsController_2024_04_15 {
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string,
|
||||
isEmbed?: string
|
||||
): Promise<NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams> {
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const clone = { ...req };
|
||||
const owner = await this.getOwner(req);
|
||||
const userId = owner?.id ?? -1;
|
||||
const userUuid = owner?.uuid;
|
||||
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(oAuthClientId, this.transformToBoolean(isEmbed))
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
@@ -655,7 +539,6 @@ export class BookingsController_2024_04_15 {
|
||||
});
|
||||
Object.assign(clone, {
|
||||
userId,
|
||||
userUuid,
|
||||
...oAuthParams,
|
||||
platformBookingLocation,
|
||||
noEmail: !oAuthParams.arePlatformEmailsEnabled,
|
||||
@@ -664,7 +547,7 @@ export class BookingsController_2024_04_15 {
|
||||
if (oAuthClientId) {
|
||||
await this.setPlatformAttendeesEmails(clone.body, oAuthClientId);
|
||||
}
|
||||
return clone as unknown as NextApiRequest & { userId?: number; userUuid?: string } & OAuthRequestParams;
|
||||
return clone as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
|
||||
private handleBookingErrors(
|
||||
@@ -682,9 +565,6 @@ export class BookingsController_2024_04_15 {
|
||||
|
||||
if (err instanceof Error) {
|
||||
const error = err as Error;
|
||||
if (err instanceof HttpException) {
|
||||
throw new HttpException(err.getResponse(), err.getStatus());
|
||||
}
|
||||
if (Object.values(ErrorCode).includes(error.message as unknown as ErrorCode)) {
|
||||
throw new HttpException(error.message, 400);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
ValidateNested,
|
||||
isEmail,
|
||||
Validate,
|
||||
IsInt,
|
||||
} from "class-validator";
|
||||
import { ValidationOptions, registerDecorator } from "class-validator";
|
||||
|
||||
@@ -27,7 +26,7 @@ function ValidateBookingName(validationOptions?: ValidationOptions) {
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: string | Record<string, string>): boolean {
|
||||
validate(value: any) {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
@@ -195,6 +194,11 @@ export class CreateBookingInput_2024_04_15 {
|
||||
@ApiHideProperty()
|
||||
skipContactOwner?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiHideProperty()
|
||||
_shouldServeCache?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiHideProperty()
|
||||
@@ -227,16 +231,4 @@ export class CreateBookingInput_2024_04_15 {
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
crmOwnerRecordType?: string;
|
||||
|
||||
/* @ApiPropertyOptional({
|
||||
type: [Number],
|
||||
description:
|
||||
"For round robin event types, filter available hosts to only consider the specified subset of host user IDs. This allows you to book with specific hosts within a round robin event type.",
|
||||
example: [1, 2, 3],
|
||||
}) */
|
||||
@ApiHideProperty()
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
rrHostSubsetIds?: number[];
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
allRecurringDates?: { start: string; end: string | undefined }[];
|
||||
allRecurringDates?: Record<string, string>[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
|
||||
+1
-2
@@ -9,7 +9,7 @@ export class BookingReferencesRepository_2024_08_13 {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
|
||||
async getBookingReferences(bookingId: number, filter?: BookingReferencesFilterInput_2024_08_13) {
|
||||
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId, deleted: null };
|
||||
const whereClause: Prisma.BookingReferenceWhereInput = { bookingId };
|
||||
|
||||
if (filter?.type) {
|
||||
whereClause.type = filter.type;
|
||||
@@ -30,7 +30,6 @@ export class BookingReferencesRepository_2024_08_13 {
|
||||
return this.dbRead.prisma.bookingReference.findFirst({
|
||||
where: {
|
||||
uid: eventUid,
|
||||
deleted: null,
|
||||
},
|
||||
include: {
|
||||
credential: true,
|
||||
@@ -1,12 +1,8 @@
|
||||
import { BookingGuestsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/booking-guests.controller";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { BookingsController_2024_08_13 } from "@/ee/bookings/2024-08-13/controllers/bookings.controller";
|
||||
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/repositories/bookings.repository";
|
||||
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
|
||||
import { BookingReferencesService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-references.service";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { CalVideoOutputService } from "@/ee/bookings/2024-08-13/services/cal-video.output.service";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
import { ErrorsBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/errors.service";
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
@@ -19,7 +15,6 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { InstantBookingModule } from "@/lib/modules/instant-booking.module";
|
||||
import { RecurringBookingModule } from "@/lib/modules/recurring-booking.module";
|
||||
@@ -76,11 +71,9 @@ import { Module } from "@nestjs/common";
|
||||
OAuthClientRepository,
|
||||
OAuthClientUsersService,
|
||||
BookingsService_2024_08_13,
|
||||
BookingGuestsService_2024_08_13,
|
||||
InputBookingsService_2024_08_13,
|
||||
OutputBookingsService_2024_08_13,
|
||||
OutputBookingReferencesService_2024_08_13,
|
||||
OutputEventTypesService_2024_06_14,
|
||||
BookingsRepository_2024_08_13,
|
||||
EventTypesRepository_2024_06_14,
|
||||
BookingSeatRepository,
|
||||
@@ -98,10 +91,8 @@ import { Module } from "@nestjs/common";
|
||||
BookingReferencesService_2024_08_13,
|
||||
BookingReferencesRepository_2024_08_13,
|
||||
CalVideoService,
|
||||
CalVideoOutputService,
|
||||
BookingPbacGuard,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13, BookingGuestsController_2024_08_13],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
})
|
||||
export class BookingsModule_2024_08_13 {}
|
||||
|
||||
-25
@@ -50,20 +50,6 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithUserIdAndSeatsReferencesCount(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
seatsReferences: {
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUid(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
@@ -72,17 +58,6 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithEventType(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
include: {
|
||||
eventType: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithUser(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
@@ -1,67 +0,0 @@
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { AddGuestsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-guests.output";
|
||||
import { BookingGuestsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/booking-guests.service";
|
||||
import { VERSION_2024_08_13_VALUE, VERSION_2024_08_13 } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { Controller, Post, Logger, Body, UseGuards, Param, HttpCode, HttpStatus } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags, ApiHeader } from "@nestjs/swagger";
|
||||
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { AddGuestsInput_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/bookings/:bookingUid/guests",
|
||||
version: VERSION_2024_08_13_VALUE,
|
||||
})
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Bookings / Guests")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to ${VERSION_2024_08_13}. This header is required as this endpoint does not exist in older API versions.`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
})
|
||||
export class BookingGuestsController_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingGuestsController_2024_08_13");
|
||||
|
||||
constructor(private readonly bookingGuestsService: BookingGuestsService_2024_08_13) {}
|
||||
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Add guests to an existing booking",
|
||||
description: `Add one or more guests to an existing booking.
|
||||
|
||||
**Email Notifications:**
|
||||
When guests are added, the following notifications are sent (unless disabled by event type settings):
|
||||
|
||||
- **Organizer & Team Members:** Receive an "Add Guests" notification email informing them that new guests have been added to the booking.
|
||||
|
||||
- **New Guests:** Receive a "Scheduled Event" email with full booking details and calendar invite. If they have a phone number, they also receive an SMS notification.
|
||||
|
||||
- **Existing Guests:** Receive an "Add Guests" notification email informing them that additional guests have been added to the booking.
|
||||
|
||||
<Note>The cal-api-version header is required for this endpoint. Without it, the request will fail with a 404 error.</Note>
|
||||
`,
|
||||
})
|
||||
async addGuests(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: AddGuestsInput_2024_08_13,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<AddGuestsOutput_2024_08_13> {
|
||||
const booking = await this.bookingGuestsService.addGuests(bookingUid, body, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { BookingPbacGuard } from "@/ee/bookings/2024-08-13/guards/booking-pbac.guard";
|
||||
import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.guard";
|
||||
import { BookingReferencesFilterInput_2024_08_13 } from "@/ee/bookings/2024-08-13/inputs/booking-references-filter.input";
|
||||
import { BookingReferencesOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/booking-references.output";
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
GetOptionalUser,
|
||||
} from "@/modules/auth/decorators/get-optional-user/get-optional-user.decorator";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Pbac } from "@/modules/auth/decorators/pbac/pbac.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { OptionalApiAuthGuard } from "@/modules/auth/guards/optional-api-auth/optional-api-auth.guard";
|
||||
@@ -68,7 +66,6 @@ import {
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
GetBookingRecordingsOutput,
|
||||
GetBookingTranscriptsOutput,
|
||||
GetBookingVideoSessionsOutput,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInputPipe,
|
||||
@@ -137,8 +134,6 @@ export class BookingsController_2024_08_13 {
|
||||
If you are creating a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
|
||||
For event types that have SMS reminders workflow, you need to pass the attendee's phone number in the request body via \`attendee.phoneNumber\` (e.g., "+19876543210" in international format). This is an optional field, but becomes required when SMS reminders are enabled for the event type. For the complete attendee object structure, see the [attendee object](https://cal.com/docs/api-reference/v2/bookings/create-a-booking#body-attendee) documentation.
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -192,7 +187,7 @@ export class BookingsController_2024_08_13 {
|
||||
2. uid of one of the recurring booking recurrences
|
||||
|
||||
3. uid of recurring booking which will return an array of all recurring booking recurrences (stored as recurringBookingUid on one of the individual recurrences).
|
||||
|
||||
|
||||
If you are fetching a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
|
||||
@@ -212,16 +207,12 @@ export class BookingsController_2024_08_13 {
|
||||
}
|
||||
|
||||
@Get("/:bookingUid/recordings")
|
||||
// @Pbac(["booking.readRecordings"])
|
||||
@Permissions([BOOKING_READ])
|
||||
@UseGuards(BookingUidGuard)
|
||||
// @UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get all the recordings for the booking",
|
||||
description: `Fetches all the recordings for the booking \`:bookingUid\`. Requires authentication and proper authorization. Access is granted if you are the booking organizer, team admin or org admin/owner.
|
||||
|
||||
<Note>cal-api-version: \`2024-08-13\` is required in the request header.</Note>
|
||||
description: `Fetches all the recordings for the booking \`:bookingUid\`
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
async getBookingRecordings(@Param("bookingUid") bookingUid: string): Promise<GetBookingRecordingsOutput> {
|
||||
@@ -230,26 +221,16 @@ export class BookingsController_2024_08_13 {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: recordings,
|
||||
message:
|
||||
"This endpoint will require authentication in a future release. Please update your integration to include valid credentials. See https://cal.com/docs/api-reference/v2/introduction#authentication for details.",
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:bookingUid/transcripts")
|
||||
// @Pbac(["booking.readRecordings"])
|
||||
@Permissions([BOOKING_READ])
|
||||
@UseGuards(BookingUidGuard)
|
||||
// @UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get Cal Video real time transcript download links for the booking",
|
||||
description: `Fetches all the transcript download links for the booking \`:bookingUid\`
|
||||
|
||||
<Note>
|
||||
Transcripts are generated when clicking "Transcribe" during a Cal Video meeting. Download links are valid for 1 hour only - make a new request to generate fresh links after expiration.
|
||||
|
||||
Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.
|
||||
</Note>
|
||||
summary: "Get all the transcripts download links for the booking",
|
||||
description: `Fetches all the transcripts download links for the booking \`:bookingUid\`
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
async getBookingTranscripts(@Param("bookingUid") bookingUid: string): Promise<GetBookingTranscriptsOutput> {
|
||||
@@ -258,8 +239,6 @@ export class BookingsController_2024_08_13 {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: transcripts ?? [],
|
||||
message:
|
||||
"This endpoint will require authentication in a future release. Please update your integration to include valid credentials. See https://cal.com/docs/api-reference/v2/introduction#authentication for details.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -298,7 +277,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Reschedule a booking",
|
||||
description: `Reschedule a booking or seated booking
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -309,7 +288,7 @@ export class BookingsController_2024_08_13 {
|
||||
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2). If you're rescheduling a seated booking as org admin of booking host, pass booking input for Reschedule Booking (Option 1) along with your access token in the request header.
|
||||
description: `Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2).
|
||||
|
||||
If you are rescheduling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.`,
|
||||
@@ -340,19 +319,16 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Cancel a booking",
|
||||
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
|
||||
|
||||
\nCancelling normal bookings:
|
||||
If the booking is not seated and not recurring, simply pass :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and optionally cancellationReason in the request body \`{"cancellationReason": "Will travel"}\`.
|
||||
|
||||
|
||||
\nCancelling seated bookings:
|
||||
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
|
||||
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
|
||||
\n2. As the host or org admin of host - host can cancel booking for all attendees aka for every seat, this also applies to org admins. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
|
||||
|
||||
\n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
|
||||
|
||||
\nCancelling recurring seated bookings:
|
||||
For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.
|
||||
|
||||
|
||||
If you are cancelling a seated booking for an event type with 'show attendees' disabled, then to retrieve attendees in the response either set 'show attendees' to true on event type level or
|
||||
you have to provide an authentication method of event type owner, host, team admin or owner or org admin or owner.
|
||||
|
||||
@@ -367,7 +343,7 @@ export class BookingsController_2024_08_13 {
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of cancel booking input: Cancel Booking (Option 1 which is for normal or recurring bookings) or Cancel Seated Booking (Option 2 which is for seated bookings)",
|
||||
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
|
||||
})
|
||||
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
|
||||
async cancelBooking(
|
||||
@@ -393,22 +369,22 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Mark a booking absence",
|
||||
description: `The provided authorization header refers to the owner of the booking.
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
async markNoShow(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: MarkAbsentBookingInput_2024_08_13,
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<MarkAbsentBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.markAbsent(bookingUid, user.id, body, user.uuid);
|
||||
async markNoShow(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: MarkAbsentBookingInput_2024_08_13,
|
||||
@GetUser("id") ownerId: number
|
||||
): Promise<MarkAbsentBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.markAbsent(bookingUid, ownerId, body);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/reassign")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@@ -418,15 +394,15 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Reassign a booking to auto-selected host",
|
||||
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
async reassignBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() reassignedByUser: ApiAuthGuardUser
|
||||
@GetUser() user: ApiAuthGuardUser
|
||||
): Promise<ReassignBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.reassignBooking(bookingUid, reassignedByUser);
|
||||
const booking = await this.bookingsService.reassignBooking(bookingUid, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
@@ -442,20 +418,20 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Reassign a booking to a specific host",
|
||||
description: `Currently only supports reassigning host for round robin bookings. The provided authorization header refers to the owner of the booking.
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
async reassignBookingToUser(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Param("userId") userId: number,
|
||||
@GetUser() reassignedByUser: ApiAuthGuardUser,
|
||||
@GetUser("id") reassignedById: number,
|
||||
@Body() body: ReassignToUserBookingInput_2024_08_13
|
||||
): Promise<ReassignBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.reassignBookingToUser(
|
||||
bookingUid,
|
||||
userId,
|
||||
reassignedByUser,
|
||||
reassignedById,
|
||||
body
|
||||
);
|
||||
|
||||
@@ -473,7 +449,7 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Confirm a booking",
|
||||
description: `The provided authorization header refers to the owner of the booking.
|
||||
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@@ -497,8 +473,8 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Decline a booking",
|
||||
description: `The provided authorization header refers to the owner of the booking.
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
async declineBooking(
|
||||
@@ -521,8 +497,8 @@ export class BookingsController_2024_08_13 {
|
||||
@ApiOperation({
|
||||
summary: "Get 'Add to Calendar' links for a booking",
|
||||
description: `Retrieve calendar links for a booking that can be used to add the event to various calendar services. Returns links for Google Calendar, Microsoft Office, Microsoft Outlook, and a downloadable ICS file.
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
|
||||
<Note>Please make sure to pass in the cal-api-version header value as mentioned in the Headers section. Not passing the correct value will default to an older version of this endpoint.</Note>
|
||||
`,
|
||||
})
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@@ -561,25 +537,4 @@ export class BookingsController_2024_08_13 {
|
||||
data: bookingReferences,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:bookingUid/conferencing-sessions")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Pbac(["booking.readRecordings"])
|
||||
@Permissions([BOOKING_READ])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard, BookingPbacGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get Video Meeting Sessions. Only supported for Cal Video",
|
||||
description: `Requires authentication and proper authorization. Access is granted if you are the booking organizer, team admin or org admin/owner.
|
||||
|
||||
<Note>cal-api-version: \`2024-08-13\` is required in the request header.</Note>`,
|
||||
})
|
||||
async getVideoSessions(@Param("bookingUid") bookingUid: string): Promise<GetBookingVideoSessionsOutput> {
|
||||
const sessions = await this.calVideoService.getVideoSessions(bookingUid);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: sessions,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,523 +0,0 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { AddGuestsOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/add-guests.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
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 * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.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 { randomString } from "test/utils/randomString";
|
||||
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
ERROR_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_08_13,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
AttendeeAddGuestsEmail,
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerAddGuestsEmail,
|
||||
} from "@calcom/platform-libraries/emails";
|
||||
import type { BookingOutput_2024_08_13, CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import type { User, Team } from "@calcom/prisma/client";
|
||||
|
||||
const attendeeAddGuestsEmailSpy = jest
|
||||
.spyOn(AttendeeAddGuestsEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
const organizerAddGuestsEmailSpy = jest
|
||||
.spyOn(OrganizerAddGuestsEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
const attendeeScheduledEmailSpy = jest
|
||||
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
type TestUser = {
|
||||
user: User;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
};
|
||||
|
||||
type TestSetup = {
|
||||
organizer: TestUser;
|
||||
unrelatedUser: TestUser;
|
||||
guest: TestUser;
|
||||
eventTypeId: number;
|
||||
bookingUid: string;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 add guests", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let tokensRepositoryFixture: TokensRepositoryFixture;
|
||||
|
||||
let testSetup: TestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
tokensRepositoryFixture = new TokensRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `user-emails-2024-08-13-organization-${randomString()}`,
|
||||
});
|
||||
|
||||
await setupTestData();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function setupTestData() {
|
||||
const oAuthClient = await createOAuthClient(organization.id, true);
|
||||
|
||||
const organizerUser = await userRepositoryFixture.create({
|
||||
email: `user-emails-2024-08-13-organizer-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const unrelatedUserData = await userRepositoryFixture.create({
|
||||
email: `user-emails-2024-08-13-unrelated-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const guestUser = await userRepositoryFixture.create({
|
||||
email: `user-emails-2024-08-13-guest-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClient.id },
|
||||
},
|
||||
});
|
||||
|
||||
const organizerTokens = await tokensRepositoryFixture.createTokens(organizerUser.id, oAuthClient.id);
|
||||
const unrelatedUserTokens = await tokensRepositoryFixture.createTokens(
|
||||
unrelatedUserData.id,
|
||||
oAuthClient.id
|
||||
);
|
||||
const guestTokens = await tokensRepositoryFixture.createTokens(guestUser.id, oAuthClient.id);
|
||||
|
||||
const schedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `user-add-guests-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(organizerUser.id, schedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `user-add-guests-2024-08-13-event-type-${randomString()}`,
|
||||
slug: `user-add-guests-2024-08-13-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
testSetup = {
|
||||
organizer: {
|
||||
user: organizerUser,
|
||||
accessToken: organizerTokens.accessToken,
|
||||
refreshToken: organizerTokens.refreshToken,
|
||||
},
|
||||
unrelatedUser: {
|
||||
user: unrelatedUserData,
|
||||
accessToken: unrelatedUserTokens.accessToken,
|
||||
refreshToken: unrelatedUserTokens.refreshToken,
|
||||
},
|
||||
guest: {
|
||||
user: guestUser,
|
||||
accessToken: guestTokens.accessToken,
|
||||
refreshToken: guestTokens.refreshToken,
|
||||
},
|
||||
eventTypeId: eventType.id,
|
||||
bookingUid: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: emailsEnabled,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
attendeeAddGuestsEmailSpy.mockClear();
|
||||
organizerAddGuestsEmailSpy.mockClear();
|
||||
attendeeScheduledEmailSpy.mockClear();
|
||||
});
|
||||
|
||||
describe("POST /v2/bookings/:bookingUid/guests", () => {
|
||||
beforeAll(async () => {
|
||||
const createBookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: testSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
const createBookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(createBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
|
||||
expect(createBookingResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
if (!responseDataIsBooking(createBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
testSetup.bookingUid = createBookingResponseBody.data.uid;
|
||||
});
|
||||
|
||||
describe("Authentication", () => {
|
||||
it("should return 401 when adding guests without authentication", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "unauthenticated.guest@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(addGuestsResponse.status).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authorization - Organizer", () => {
|
||||
it("should allow booking organizer to add multiple guests", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "organizer.guest1@example.com" }, { email: "organizer.guest2@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
|
||||
|
||||
verifyAddGuestsResponse(
|
||||
addGuestsResponseBody,
|
||||
["organizer.guest1@example.com", "organizer.guest2@example.com"],
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("should return 403 when unrelated user tries to add guests", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [
|
||||
{ email: "non-organizer.guest1@example.com" },
|
||||
{ email: "non-organizer.guest2@example.com" },
|
||||
],
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authorization - guest", () => {
|
||||
it("should allow organizer to add a user as guest, then guest can add more guests", async () => {
|
||||
// Step 1: Organizer adds guest user as a guest
|
||||
const addguestBody = {
|
||||
guests: [{ email: testSetup.guest.user.email }],
|
||||
};
|
||||
|
||||
const addguestResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addguestBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const addguestResponseBody: AddGuestsOutput_2024_08_13 = addguestResponse.body;
|
||||
|
||||
verifyAddGuestsResponse(addguestResponseBody, [testSetup.guest.user.email], true);
|
||||
|
||||
// Step 2: Now the guest can add their own guests
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "guest.guest1@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.guest.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
|
||||
|
||||
verifyAddGuestsResponse(addGuestsResponseBody, ["guest.guest1@example.com"], true);
|
||||
});
|
||||
|
||||
it("should return 403 when non-guest user tries to add guests", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "non-guest.guest1@example.com" }, { email: "non-guest.guest2@example.com" }],
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.unrelatedUser.accessToken}`)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Validation", () => {
|
||||
it("should return 400 when attempting to add duplicate guest email", async () => {
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "organizer.guest1@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${testSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${testSetup.organizer.accessToken}`);
|
||||
|
||||
expect(addGuestsResponse.status).toEqual(400);
|
||||
expect(addGuestsResponse.body.status).toEqual(ERROR_STATUS);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /v2/bookings/:bookingUid/guests - Emails Disabled", () => {
|
||||
let emailsDisabledSetup: TestSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
const oAuthClientDisabled = await createOAuthClient(organization.id, false);
|
||||
|
||||
const organizerUser = await userRepositoryFixture.create({
|
||||
email: `user-no-emails-2024-08-13-organizer-${randomString()}@api.com`,
|
||||
platformOAuthClients: {
|
||||
connect: { id: oAuthClientDisabled.id },
|
||||
},
|
||||
});
|
||||
|
||||
const organizerTokens = await tokensRepositoryFixture.createTokens(
|
||||
organizerUser.id,
|
||||
oAuthClientDisabled.id
|
||||
);
|
||||
|
||||
const schedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `user-no-emails-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(organizerUser.id, schedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `user-no-emails-2024-08-13-event-type-${randomString()}`,
|
||||
slug: `user-no-emails-2024-08-13-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
},
|
||||
organizerUser.id
|
||||
);
|
||||
|
||||
const createBookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 9, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: eventType.id,
|
||||
attendee: {
|
||||
name: "No Email User",
|
||||
email: "no.email.user@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/no-emails",
|
||||
bookingFieldsResponses: {
|
||||
customField: "noEmailValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "200",
|
||||
},
|
||||
};
|
||||
|
||||
const createBookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(createBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const createBookingResponseBody: CreateBookingOutput_2024_08_13 = createBookingResponse.body;
|
||||
|
||||
if (!responseDataIsBooking(createBookingResponseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
emailsDisabledSetup = {
|
||||
organizer: {
|
||||
user: organizerUser,
|
||||
accessToken: organizerTokens.accessToken,
|
||||
refreshToken: organizerTokens.refreshToken,
|
||||
},
|
||||
unrelatedUser: testSetup.unrelatedUser,
|
||||
guest: testSetup.guest,
|
||||
eventTypeId: eventType.id,
|
||||
bookingUid: createBookingResponseBody.data.uid,
|
||||
};
|
||||
});
|
||||
|
||||
it("should NOT send emails when adding guests (emails disabled)", async () => {
|
||||
attendeeAddGuestsEmailSpy.mockClear();
|
||||
organizerAddGuestsEmailSpy.mockClear();
|
||||
attendeeScheduledEmailSpy.mockClear();
|
||||
|
||||
const addGuestsBody = {
|
||||
guests: [{ email: "no-email.guest1@example.com" }, { email: "no-email.guest2@example.com" }],
|
||||
};
|
||||
|
||||
const addGuestsResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.bookingUid}/guests`)
|
||||
.send(addGuestsBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${emailsDisabledSetup.organizer.accessToken}`)
|
||||
.expect(200);
|
||||
|
||||
const addGuestsResponseBody: AddGuestsOutput_2024_08_13 = addGuestsResponse.body;
|
||||
|
||||
verifyAddGuestsResponse(
|
||||
addGuestsResponseBody,
|
||||
["no-email.guest1@example.com", "no-email.guest2@example.com"],
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.organizer.user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
emailsDisabledSetup.organizer.user.id,
|
||||
emailsDisabledSetup.organizer.user.email
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.organizer.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.unrelatedUser.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(testSetup.guest.user.email);
|
||||
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
testSetup.organizer.user.id,
|
||||
testSetup.organizer.user.email
|
||||
);
|
||||
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && "id" in data;
|
||||
}
|
||||
|
||||
function verifyAddGuestsResponse(
|
||||
responseBody: AddGuestsOutput_2024_08_13,
|
||||
expectedGuestEmails: string[],
|
||||
shouldEmailsBeSent: boolean
|
||||
): BookingOutput_2024_08_13 {
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
if (!responseDataIsBooking(responseBody.data)) {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
|
||||
const bookingData = responseBody.data;
|
||||
|
||||
expect(bookingData.guests).toBeDefined();
|
||||
const guestEmails = bookingData.guests;
|
||||
|
||||
expectedGuestEmails.forEach((email) => {
|
||||
expect(guestEmails).toContain(email);
|
||||
});
|
||||
|
||||
expect(bookingData.guests).toBeDefined();
|
||||
expectedGuestEmails.forEach((email) => {
|
||||
expect(bookingData.guests).toContain(email);
|
||||
});
|
||||
|
||||
if (shouldEmailsBeSent) {
|
||||
expect(attendeeAddGuestsEmailSpy).toHaveBeenCalled();
|
||||
expect(organizerAddGuestsEmailSpy).toHaveBeenCalled();
|
||||
expect(attendeeScheduledEmailSpy).toHaveBeenCalled();
|
||||
} else {
|
||||
expect(attendeeAddGuestsEmailSpy).not.toHaveBeenCalled();
|
||||
expect(organizerAddGuestsEmailSpy).not.toHaveBeenCalled();
|
||||
expect(attendeeScheduledEmailSpy).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
return bookingData;
|
||||
}
|
||||
});
|
||||
-245
@@ -1,245 +0,0 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
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 * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import type { Booking, User, PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Booking access authorization", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
|
||||
const ownerEmail = `booking-access-auth-owner-${randomString()}@api.com`;
|
||||
const unauthorizedEmail = `booking-access-auth-unauthorized-${randomString()}@api.com`;
|
||||
let ownerUser: User;
|
||||
let unauthorizedUser: User;
|
||||
let ownerApiKey: string;
|
||||
let unauthorizedApiKey: string;
|
||||
|
||||
let eventTypeId: number;
|
||||
let testBooking: Booking;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({
|
||||
name: `booking-access-auth-organization-${randomString()}`,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
ownerUser = await userRepositoryFixture.create({
|
||||
email: ownerEmail,
|
||||
locale: "en",
|
||||
name: `booking-access-auth-owner-${randomString()}`,
|
||||
});
|
||||
|
||||
unauthorizedUser = await userRepositoryFixture.create({
|
||||
email: unauthorizedEmail,
|
||||
locale: "en",
|
||||
name: `booking-access-auth-unauthorized-${randomString()}`,
|
||||
});
|
||||
|
||||
const { keyString: ownerKeyString } = await apiKeysRepositoryFixture.createApiKey(ownerUser.id, null);
|
||||
ownerApiKey = `cal_test_${ownerKeyString}`;
|
||||
|
||||
const { keyString: unauthorizedKeyString } = await apiKeysRepositoryFixture.createApiKey(
|
||||
unauthorizedUser.id,
|
||||
null
|
||||
);
|
||||
unauthorizedApiKey = `cal_test_${unauthorizedKeyString}`;
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `booking-access-auth-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(ownerUser.id, userSchedule);
|
||||
|
||||
const eventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `booking-access-auth-event-type-${randomString()}`,
|
||||
slug: `booking-access-auth-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
},
|
||||
ownerUser.id
|
||||
);
|
||||
eventTypeId = eventType.id;
|
||||
|
||||
testBooking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-access-auth-booking-${randomString()}`,
|
||||
title: "Test Booking for Access Auth",
|
||||
description: "",
|
||||
startTime: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)),
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: ownerUser.id,
|
||||
},
|
||||
},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Test Attendee",
|
||||
email: "attendee@example.com",
|
||||
},
|
||||
references: {
|
||||
create: [
|
||||
{
|
||||
type: "daily_video",
|
||||
uid: `daily-room-${randomString()}`,
|
||||
meetingId: `daily-room-${randomString()}`,
|
||||
meetingPassword: "test-password",
|
||||
meetingUrl: `https://daily.co/test-room-${randomString()}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("GET /v2/bookings/:bookingUid/conferencing-sessions - Authorization", () => {
|
||||
it("should allow booking organizer to access conferencing sessions", async () => {
|
||||
const calVideoService = app.get(CalVideoService);
|
||||
jest.spyOn(calVideoService, "getVideoSessions").mockResolvedValue([]);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testBooking.uid}/conferencing-sessions`)
|
||||
.set("Authorization", `Bearer ${ownerApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
|
||||
it("should return 403 when unauthorized user tries to access conferencing sessions", async () => {
|
||||
await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${testBooking.uid}/conferencing-sessions`)
|
||||
.set("Authorization", `Bearer ${unauthorizedApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
// describe("GET /v2/bookings/:bookingUid/recordings - Authorization", () => {
|
||||
// it("should allow booking organizer to access recordings", async () => {
|
||||
// const calVideoService = app.get(CalVideoService);
|
||||
// jest.spyOn(calVideoService, "getRecordings").mockResolvedValue([]);
|
||||
|
||||
// const response = await request(app.getHttpServer())
|
||||
// .get(`/v2/bookings/${testBooking.uid}/recordings`)
|
||||
// .set("Authorization", `Bearer ${ownerApiKey}`)
|
||||
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
// .expect(200);
|
||||
|
||||
// expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
// });
|
||||
|
||||
// it("should return 403 when unauthorized user tries to access recordings", async () => {
|
||||
// await request(app.getHttpServer())
|
||||
// .get(`/v2/bookings/${testBooking.uid}/recordings`)
|
||||
// .set("Authorization", `Bearer ${unauthorizedApiKey}`)
|
||||
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
// .expect(403);
|
||||
// });
|
||||
// });
|
||||
|
||||
// describe("GET /v2/bookings/:bookingUid/transcripts - Authorization", () => {
|
||||
// it("should allow booking organizer to access transcripts", async () => {
|
||||
// const calVideoService = app.get(CalVideoService);
|
||||
// jest.spyOn(calVideoService, "getTranscripts").mockResolvedValue([]);
|
||||
|
||||
// const response = await request(app.getHttpServer())
|
||||
// .get(`/v2/bookings/${testBooking.uid}/transcripts`)
|
||||
// .set("Authorization", `Bearer ${ownerApiKey}`)
|
||||
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
// .expect(200);
|
||||
|
||||
// expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
// });
|
||||
|
||||
// it("should return 403 when unauthorized user tries to access transcripts", async () => {
|
||||
// await request(app.getHttpServer())
|
||||
// .get(`/v2/bookings/${testBooking.uid}/transcripts`)
|
||||
// .set("Authorization", `Bearer ${unauthorizedApiKey}`)
|
||||
// .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
// .expect(403);
|
||||
// });
|
||||
// });
|
||||
|
||||
afterAll(async () => {
|
||||
await bookingsRepositoryFixture.deleteById(testBooking.id);
|
||||
await eventTypesRepositoryFixture.delete(eventTypeId);
|
||||
await userRepositoryFixture.deleteByEmail(ownerEmail);
|
||||
await userRepositoryFixture.deleteByEmail(unauthorizedEmail);
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,11 @@ import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import { CreateBookingInput_2024_08_13, GetBookingOutput_2024_08_13, GetSeatedBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import type { Booking, PlatformOAuthClient, Team, User, EventType } from "@calcom/prisma/client";
|
||||
|
||||
@@ -850,7 +854,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
describe("Booking Field Type Validation", () => {
|
||||
const basePayload = {
|
||||
start: new Date(Date.UTC(2030, 5, 19, 11, 0, 0)).toISOString(),
|
||||
start: "2025-06-19T11:00:00.000Z",
|
||||
attendee: {
|
||||
name: "Charlie TypeTest",
|
||||
email: "charlie.typetest@example.com",
|
||||
@@ -1158,31 +1162,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
`One or more invalid options for booking field '${fieldName}'. Allowed options are: blue, red.`
|
||||
);
|
||||
});
|
||||
|
||||
it("should transform null values to empty strings in bookingFieldsResponses", async () => {
|
||||
const payload = {
|
||||
...basePayload,
|
||||
eventTypeId: eventTypeWithBookingFields.id,
|
||||
bookingFieldsResponses: {
|
||||
"favorite-movie": "The Matrix",
|
||||
rescheduleReason: null,
|
||||
notes: null,
|
||||
},
|
||||
};
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings`)
|
||||
.send(payload)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
if (responseDataIsBooking(response.body.data)) {
|
||||
const data: BookingOutput_2024_08_13 = response.body.data;
|
||||
expect(data.bookingFieldsResponses.notes).toBe("");
|
||||
expect(data.bookingFieldsResponses.rescheduleReason).toBe("");
|
||||
expect(data.bookingFieldsResponses["favorite-movie"]).toBe("The Matrix");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -1197,4 +1176,4 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+17
-18
@@ -12,7 +12,6 @@ import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
@@ -21,6 +20,7 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
@@ -72,7 +72,6 @@ type EmailSetup = {
|
||||
team: Team;
|
||||
member1: User;
|
||||
member2: User;
|
||||
member1ApiKey: string;
|
||||
collectiveEventType: { id: number };
|
||||
roundRobinEventType: { id: number };
|
||||
};
|
||||
@@ -90,21 +89,25 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
|
||||
// Setup data for tests
|
||||
let emailsEnabledSetup: EmailSetup;
|
||||
let emailsDisabledSetup: EmailSetup;
|
||||
|
||||
const authEmail = "team-emails-2024-08-13-user-admin@example.com";
|
||||
|
||||
// Utility function to check response data type
|
||||
const responseDataIsBooking = (data: unknown): data is BookingOutput_2024_08_13 => {
|
||||
return !Array.isArray(data) && data !== null && typeof data === "object" && data && "id" in data;
|
||||
const responseDataIsBooking = (data: any): data is BookingOutput_2024_08_13 => {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
const moduleRef = await withApiAuth(
|
||||
authEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
@@ -119,7 +122,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
// Create a base organization for all tests
|
||||
@@ -129,6 +131,11 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
emailsEnabledSetup = await setupTestEnvironment(true);
|
||||
emailsDisabledSetup = await setupTestEnvironment(false);
|
||||
|
||||
await userRepositoryFixture.create({
|
||||
email: authEmail,
|
||||
organization: { connect: { id: organization.id } },
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
await app.init();
|
||||
@@ -166,15 +173,10 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
const collectiveEvent = await createEventType("COLLECTIVE", team.id, [member1.id, member2.id]);
|
||||
const roundRobinEvent = await createEventType("ROUND_ROBIN", team.id, [member1.id, member2.id]);
|
||||
|
||||
// Create API key for member1 to use in authorized tests
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(member1.id, null);
|
||||
const member1ApiKey = `cal_test_${keyString}`;
|
||||
|
||||
return {
|
||||
team,
|
||||
member1,
|
||||
member2,
|
||||
member1ApiKey,
|
||||
collectiveEventType: { id: collectiveEvent.id },
|
||||
roundRobinEventType: { id: roundRobinEvent.id },
|
||||
};
|
||||
@@ -349,7 +351,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
: emailsDisabledSetup.member1.id;
|
||||
const manualReassignResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBookingUid}/reassign/${reassignToId}`)
|
||||
.set("Authorization", `Bearer ${emailsDisabledSetup.member1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(manualReassignResponse.status).toBe(200);
|
||||
@@ -358,7 +359,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
// --- 4. Automatic Reassign ---
|
||||
const autoReassignResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBookingUid}/reassign`)
|
||||
.set("Authorization", `Bearer ${emailsDisabledSetup.member1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(autoReassignResponse.status).toBe(200);
|
||||
@@ -484,7 +484,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
|
||||
const manualReassignResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBookingUid}/reassign/${reassignToId}`)
|
||||
.set("Authorization", `Bearer ${emailsEnabledSetup.member1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(manualReassignResponse.status).toBe(200);
|
||||
@@ -500,7 +499,6 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
jest.clearAllMocks();
|
||||
const autoReassignResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBookingUid}/reassign`)
|
||||
.set("Authorization", `Bearer ${emailsEnabledSetup.member1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13);
|
||||
|
||||
expect(autoReassignResponse.status).toBe(200);
|
||||
@@ -525,6 +523,7 @@ describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
afterAll(async () => {
|
||||
// Clean up database records
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(authEmail);
|
||||
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member1.email);
|
||||
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member2.email);
|
||||
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member1.email);
|
||||
|
||||
-116
@@ -70,7 +70,6 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
|
||||
let firstManagedUserEventTypeId: number;
|
||||
let eventTypeRequiresConfirmationId: number;
|
||||
let seatedEventTypeId: number;
|
||||
|
||||
const orgAdminManagedUserEmail = `managed-user-bookings-2024-08-13-org-admin-${randomString()}@api.com`;
|
||||
let orgAdminManagedUser: CreateManagedUserData;
|
||||
@@ -310,20 +309,6 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
eventTypeRequiresConfirmationId = eventTypeRequiresConfirmation.id;
|
||||
});
|
||||
|
||||
it("should create a seated event type for first managed user", async () => {
|
||||
const seatedEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `managed-user-bookings-seated-event-type-${randomString()}`,
|
||||
slug: `managed-user-bookings-seated-event-type-${randomString()}`,
|
||||
length: 30,
|
||||
seatsPerTimeSlot: 5,
|
||||
seatsShowAttendees: true,
|
||||
},
|
||||
firstManagedUser.user.id
|
||||
);
|
||||
seatedEventTypeId = seatedEventType.id;
|
||||
});
|
||||
|
||||
describe("bookings using original emails", () => {
|
||||
it("managed user should be booked by managed user attendee and booking shows up in both users' bookings", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
@@ -788,107 +773,6 @@ describe("Managed user bookings 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("seated booking management by org admin", () => {
|
||||
let seatedBookingUid: string;
|
||||
|
||||
it("should create a seated booking with multiple attendees", async () => {
|
||||
const bodyOne: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeId,
|
||||
attendee: {
|
||||
name: secondManagedUser.user.name!,
|
||||
email: secondManagedUser.user.email,
|
||||
timeZone: secondManagedUser.user.timeZone,
|
||||
language: secondManagedUser.user.locale,
|
||||
},
|
||||
};
|
||||
|
||||
const responseOne = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bodyOne)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const responseBodyForFirstAttendee: ApiSuccessResponse<BookingOutput_2024_08_13> = responseOne.body;
|
||||
expect(responseBodyForFirstAttendee.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBodyForFirstAttendee.data).toBeDefined();
|
||||
|
||||
const bookingDataForFirstAttendee = responseBodyForFirstAttendee.data as BookingOutput_2024_08_13;
|
||||
seatedBookingUid = bookingDataForFirstAttendee.uid;
|
||||
expect(bookingDataForFirstAttendee.attendees).toBeDefined();
|
||||
expect(bookingDataForFirstAttendee.attendees.length).toBeGreaterThan(0);
|
||||
|
||||
const bodyTwo: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 10, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: seatedEventTypeId,
|
||||
attendee: {
|
||||
name: thirdManagedUser.user.name!,
|
||||
email: thirdManagedUser.user.email,
|
||||
timeZone: thirdManagedUser.user.timeZone,
|
||||
language: thirdManagedUser.user.locale,
|
||||
},
|
||||
};
|
||||
|
||||
const responseTwo = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bodyTwo)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const responseBodyForSecondAttendee: ApiSuccessResponse<BookingOutput_2024_08_13> = responseTwo.body;
|
||||
expect(responseBodyForSecondAttendee.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBodyForSecondAttendee.data).toBeDefined();
|
||||
|
||||
const bookingDataForSecondAttendee = responseBodyForSecondAttendee.data as BookingOutput_2024_08_13;
|
||||
expect(bookingDataForSecondAttendee.attendees).toBeDefined();
|
||||
expect(bookingDataForSecondAttendee.attendees.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should allow org admin to reschedule all seats in a seated booking for a managed user", async () => {
|
||||
const newStartTime = new Date(Date.UTC(2030, 0, 10, 11, 0, 0)).toISOString();
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${seatedBookingUid}/reschedule`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||
.send({
|
||||
start: newStartTime,
|
||||
})
|
||||
.expect(201);
|
||||
|
||||
const responseBody: ApiSuccessResponse<BookingOutput_2024_08_13> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||
expect(bookingData.start).toEqual(newStartTime);
|
||||
|
||||
seatedBookingUid = bookingData.uid;
|
||||
});
|
||||
|
||||
it("should allow org admin to cancel all seats in a seated booking for a managed user", async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${seatedBookingUid}/cancel`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.set("Authorization", `Bearer ${orgAdminManagedUser.accessToken}`)
|
||||
.send({
|
||||
cancellationReason: "Org admin cancelled the booking",
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const bookingData = responseBody.data as BookingOutput_2024_08_13;
|
||||
expect(bookingData.status).toEqual("cancelled");
|
||||
expect(bookingData.uid).toEqual(seatedBookingUid);
|
||||
|
||||
const cancelledBooking = await bookingsRepositoryFixture.getByUid(seatedBookingUid);
|
||||
expect(cancelledBooking?.status).toEqual("CANCELLED");
|
||||
});
|
||||
});
|
||||
|
||||
describe("event type booking requires authentication", () => {
|
||||
let eventTypeRequiringAuthenticationId: number;
|
||||
|
||||
|
||||
+10
-485
@@ -1,7 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
||||
import { BOOKING_REASSIGN_PERMISSION_ERROR } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
@@ -12,7 +11,6 @@ import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||
@@ -23,6 +21,7 @@ import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repo
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import type { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
@@ -45,34 +44,23 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
|
||||
const teamUserEmail = `reassign-bookings-2024-08-13-user1-${randomString()}@api.com`;
|
||||
const teamUserEmail2 = `reassign-bookings-2024-08-13-user2-${randomString()}@api.com`;
|
||||
const teamUserEmail3 = `reassign-bookings-2024-08-13-user3-${randomString()}@api.com`;
|
||||
let teamUser1: User;
|
||||
let teamUser2: User;
|
||||
let teamUser3: User;
|
||||
let teamUser1ApiKey: string;
|
||||
let teamUser2ApiKey: string;
|
||||
|
||||
let teamRoundRobinEventTypeId: number;
|
||||
let teamRoundRobinFixedHostEventTypeId: number;
|
||||
let teamRoundRobinNonFixedEventTypeId: number;
|
||||
let teamRoundRobinWithRescheduleReasonEventTypeId: number;
|
||||
|
||||
let teamRoundRobinNonFixedEventTypeTitle: string;
|
||||
let teamRoundRobinFixedHostEventTypeTitle: string;
|
||||
let teamRoundRobinWithRescheduleReasonEventTypeTitle: string;
|
||||
|
||||
let roundRobinBooking: Booking;
|
||||
let rescheduleReasonBookingUid: string;
|
||||
let rescheduleReasonBookingInitialHostId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
const moduleRef = await withApiAuth(
|
||||
teamUserEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
@@ -88,7 +76,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({
|
||||
@@ -109,43 +96,23 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
teamUser1 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail,
|
||||
locale: "en",
|
||||
locale: "it",
|
||||
name: `reassign-bookings-2024-08-13-user1-${randomString()}`,
|
||||
});
|
||||
|
||||
teamUser2 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail2,
|
||||
locale: "en",
|
||||
locale: "it",
|
||||
name: `reassign-bookings-2024-08-13-user2-${randomString()}`,
|
||||
});
|
||||
|
||||
teamUser3 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail3,
|
||||
locale: "en",
|
||||
name: `reassign-bookings-2024-08-13-user3-${randomString()}`,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(teamUser1.id, null);
|
||||
teamUser1ApiKey = `cal_test_${keyString}`;
|
||||
|
||||
const { keyString: keyString2 } = await apiKeysRepositoryFixture.createApiKey(teamUser2.id, null);
|
||||
teamUser2ApiKey = `cal_test_${keyString2}`;
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `reassign-bookings-2024-08-13-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
availabilities: [
|
||||
{
|
||||
days: [0, 1, 2, 3, 4, 5, 6], // All days of the week
|
||||
startTime: new Date(Date.UTC(2024, 0, 1, 0, 0, 0)), // 00:00 UTC
|
||||
endTime: new Date(Date.UTC(2024, 0, 1, 23, 59, 0)), // 23:59 UTC
|
||||
},
|
||||
],
|
||||
};
|
||||
await schedulesService.createUserSchedule(teamUser1.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(teamUser2.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(teamUser3.id, userSchedule);
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teamUser1.id}`,
|
||||
@@ -177,21 +144,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teamUser3.id}`,
|
||||
username: teamUserEmail3,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser3.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
team: { connect: { id: team.id } },
|
||||
@@ -206,13 +158,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
team: { connect: { id: team.id } },
|
||||
user: { connect: { id: teamUser3.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
@@ -285,187 +230,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
create: {
|
||||
email: "bob@gmail.com",
|
||||
name: "Bob",
|
||||
locale: "en",
|
||||
locale: "it",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const teamNonFixedEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
users: {
|
||||
connect: [{ id: teamUser2.id }, { id: teamUser3.id }],
|
||||
},
|
||||
title: `reassign-bookings-2024-08-13-non-fixed-event-type-${randomString()}`,
|
||||
slug: `reassign-bookings-2024-08-13-non-fixed-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [],
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
});
|
||||
|
||||
teamRoundRobinNonFixedEventTypeId = teamNonFixedEventType.id;
|
||||
teamRoundRobinNonFixedEventTypeTitle = teamNonFixedEventType.title;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamNonFixedEventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser3.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamNonFixedEventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
const team2EventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
users: {
|
||||
connect: [{ id: teamUser1.id }, { id: teamUser2.id }, { id: teamUser3.id }],
|
||||
},
|
||||
title: `reassign-bookings-2024-08-13-fixed-event-type-${randomString()}`,
|
||||
slug: `reassign-bookings-2024-08-13-fixed-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [],
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
});
|
||||
|
||||
teamRoundRobinFixedHostEventTypeId = team2EventType.id;
|
||||
teamRoundRobinFixedHostEventTypeTitle = team2EventType.title;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team2EventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team2EventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser3.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team2EventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const teamEventTypeWithRescheduleReason = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
users: {
|
||||
connect: [{ id: teamUser1.id }, { id: teamUser2.id }],
|
||||
},
|
||||
title: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`,
|
||||
slug: `reassign-bookings-2024-08-13-reschedule-reason-event-type-${randomString()}`,
|
||||
length: 60,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [
|
||||
{
|
||||
name: "rescheduleReason",
|
||||
type: "textarea",
|
||||
defaultLabel: "Reason for rescheduling",
|
||||
required: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system",
|
||||
views: [
|
||||
{
|
||||
id: "reschedule",
|
||||
label: "Reschedule View",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
});
|
||||
|
||||
teamRoundRobinWithRescheduleReasonEventTypeId = teamEventTypeWithRescheduleReason.id;
|
||||
teamRoundRobinWithRescheduleReasonEventTypeTitle = teamEventTypeWithRescheduleReason.title;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamEventTypeWithRescheduleReason.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamEventTypeWithRescheduleReason.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
@@ -478,7 +248,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign`)
|
||||
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -518,7 +287,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser1.id}`)
|
||||
.send(body)
|
||||
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
@@ -539,247 +307,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct title when reassigning round robin booking with non-fixed host", async () => {
|
||||
const nonFixedHostBookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2050, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: teamRoundRobinNonFixedEventTypeId,
|
||||
attendee: {
|
||||
name: "Charlie",
|
||||
email: "charlie@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "en",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(nonFixedHostBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingUid = createResponse.body.data.uid;
|
||||
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
|
||||
expect(booking).toBeDefined();
|
||||
expect(booking?.userId).toEqual(teamUser2.id);
|
||||
|
||||
const expectedInitialTitle = `${teamRoundRobinNonFixedEventTypeTitle} between ${teamUser2.name} and Charlie`;
|
||||
expect(booking?.title).toEqual(expectedInitialTitle);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingUid}/reassign`)
|
||||
.set("Authorization", `Bearer ${teamUser2ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
|
||||
expect(data.bookingUid).toEqual(bookingUid);
|
||||
expect(data.reassignedTo.id).toEqual(teamUser3.id);
|
||||
|
||||
const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
expect(reassigned?.userId).toEqual(teamUser3.id);
|
||||
|
||||
const expectedReassignedTitle = `${teamRoundRobinNonFixedEventTypeTitle} between ${teamUser3.name} and Charlie`;
|
||||
expect(reassigned?.title).toEqual(expectedReassignedTitle);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct title when reassigning round robin booking with fixed host", async () => {
|
||||
const fixedHostBookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: teamRoundRobinFixedHostEventTypeId,
|
||||
attendee: {
|
||||
name: "Alice",
|
||||
email: "alice@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "en",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(fixedHostBookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingUid = createResponse.body.data.uid;
|
||||
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
|
||||
expect(booking).toBeDefined();
|
||||
expect(booking?.userId).toEqual(teamUser1.id);
|
||||
|
||||
const expectedInitialTitle = `${teamRoundRobinFixedHostEventTypeTitle} between ${team.name} and Alice`;
|
||||
expect(booking?.title).toEqual(expectedInitialTitle);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingUid}/reassign/${teamUser3.id}`)
|
||||
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
|
||||
expect(data.bookingUid).toEqual(bookingUid);
|
||||
expect(data.reassignedTo.id).toEqual(teamUser1.id);
|
||||
|
||||
const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
expect(reassigned?.userId).toEqual(teamUser1.id);
|
||||
|
||||
const expectedReassignedTitle = `${teamRoundRobinFixedHostEventTypeTitle} between ${team.name} and Alice`;
|
||||
expect(reassigned?.title).toEqual(expectedReassignedTitle);
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve attendee name when reassigning round robin host manually with rescheduleReason required", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2050, 0, 10, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: teamRoundRobinWithRescheduleReasonEventTypeId,
|
||||
attendee: {
|
||||
name: "David",
|
||||
email: "david@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "en",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingUid = createResponse.body.data.uid;
|
||||
rescheduleReasonBookingUid = bookingUid;
|
||||
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
|
||||
expect(booking).toBeDefined();
|
||||
expect(booking?.userId).toBeDefined();
|
||||
const initialHostId = booking!.userId!;
|
||||
rescheduleReasonBookingInitialHostId = initialHostId;
|
||||
|
||||
const expectedInitialTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${
|
||||
booking?.userId === teamUser1.id ? teamUser1.name : teamUser2.name
|
||||
} and David`;
|
||||
expect(booking?.title).toEqual(expectedInitialTitle);
|
||||
expect(booking?.title).not.toContain("Nameless");
|
||||
|
||||
const reassignToHostId = initialHostId === teamUser1.id ? teamUser2.id : teamUser1.id;
|
||||
const reassignToHostName = reassignToHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingUid}/reassign/${reassignToHostId}`)
|
||||
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
|
||||
expect(data.bookingUid).toEqual(bookingUid);
|
||||
expect(data.reassignedTo.id).toEqual(reassignToHostId);
|
||||
|
||||
const reassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
expect(reassigned?.userId).toEqual(reassignToHostId);
|
||||
|
||||
const expectedReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${reassignToHostName} and David`;
|
||||
expect(reassigned?.title).toEqual(expectedReassignedTitle);
|
||||
expect(reassigned?.title).not.toContain("Nameless");
|
||||
});
|
||||
});
|
||||
|
||||
it("should preserve attendee name when reassigning round robin host automatically with rescheduleReason required", async () => {
|
||||
const bookingUid = rescheduleReasonBookingUid;
|
||||
const booking = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
|
||||
expect(booking).toBeDefined();
|
||||
expect(booking?.userId).toBeDefined();
|
||||
|
||||
const currentHostId = booking!.userId!;
|
||||
const initialHostId = rescheduleReasonBookingInitialHostId;
|
||||
const initialHostName = initialHostId === teamUser1.id ? teamUser1.name : teamUser2.name;
|
||||
|
||||
expect(currentHostId).not.toEqual(initialHostId);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${bookingUid}/reassign`)
|
||||
.set("Authorization", `Bearer ${teamUser1ApiKey}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
|
||||
expect(data.bookingUid).toEqual(bookingUid);
|
||||
expect(data.reassignedTo.id).toEqual(initialHostId);
|
||||
|
||||
const autoReassigned = await bookingsRepositoryFixture.getByUid(bookingUid);
|
||||
expect(autoReassigned?.userId).toEqual(initialHostId);
|
||||
|
||||
const expectedAutoReassignedTitle = `${teamRoundRobinWithRescheduleReasonEventTypeTitle} between ${initialHostName} and David`;
|
||||
expect(autoReassigned?.title).toEqual(expectedAutoReassignedTitle);
|
||||
expect(autoReassigned?.title).not.toContain("Nameless");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 403 when unauthorized user tries to reassign booking", async () => {
|
||||
const unauthorizedUserEmail = `fake-user-${randomString()}@api.com`;
|
||||
const unauthorizedUser = await userRepositoryFixture.create({
|
||||
email: unauthorizedUserEmail,
|
||||
locale: "en",
|
||||
name: `fake-user-${randomString()}`,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
|
||||
const unauthorizedApiKeyString = `cal_test_${keyString}`;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign`)
|
||||
.set("Authorization", `Bearer ${unauthorizedApiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(403);
|
||||
|
||||
expect(response.body.error.message).toBe(BOOKING_REASSIGN_PERMISSION_ERROR);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(unauthorizedUserEmail);
|
||||
});
|
||||
|
||||
it("should return 403 when unauthorized user tries to reassign booking to specific user", async () => {
|
||||
const unauthorizedUserEmail = `fake-user-${randomString()}@api.com`;
|
||||
const unauthorizedUser = await userRepositoryFixture.create({
|
||||
email: unauthorizedUserEmail,
|
||||
locale: "en",
|
||||
name: `fake-user-${randomString()}`,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(unauthorizedUser.id, null);
|
||||
const unauthorizedApiKeyString = `cal_test_${keyString}`;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser2.id}`)
|
||||
.send({ reason: "Testing unauthorized access" })
|
||||
.set("Authorization", `Bearer ${unauthorizedApiKeyString}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(403);
|
||||
|
||||
expect(response.body.error.message).toBe(BOOKING_REASSIGN_PERMISSION_ERROR);
|
||||
|
||||
await userRepositoryFixture.deleteByEmail(unauthorizedUserEmail);
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
@@ -798,10 +325,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(teamUser1.email);
|
||||
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
|
||||
await userRepositoryFixture.deleteByEmail(teamUserEmail3);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser1.id, teamUser1.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser2.id, teamUser2.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser3.id, teamUser3.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
-230
@@ -1,230 +0,0 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
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 * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import type {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import type { EventType, User } from "@calcom/prisma/client";
|
||||
|
||||
describe("Reschedule bookings 2024-08-13", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
|
||||
let normalBookingUser: User;
|
||||
let normalBookingEventType: EventType;
|
||||
let normalBookingEventTypeId: number;
|
||||
const normalBookingUserEmail = `user-bookings-normal-user-${randomString(5)}@api.com`;
|
||||
const normalBookingEventTypeSlug = `user-bookings-normal-event-type-${randomString(5)}`;
|
||||
let oldNormalBooking: BookingOutput_2024_08_13;
|
||||
let newNormalBooking: BookingOutput_2024_08_13;
|
||||
|
||||
const RESCHEDULE_REASON = "Flying to venus that day";
|
||||
const RESCHEDULED_BY = `user-venus-bookings-rescheduler-${randomString(5)}@api.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
normalBookingUserEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
// initialize normal booking user, schedule and event type
|
||||
normalBookingUser = await userRepositoryFixture.create({
|
||||
email: normalBookingUserEmail,
|
||||
username: normalBookingUserEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: `user-bookings-normal-schedule-${randomString()}`,
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(normalBookingUser.id, userSchedule);
|
||||
|
||||
normalBookingEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: `user-bookings-normal-event-type-${randomString()}`,
|
||||
slug: normalBookingEventTypeSlug,
|
||||
length: 60,
|
||||
},
|
||||
normalBookingUser.id
|
||||
);
|
||||
normalBookingEventTypeId = normalBookingEventType.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
await app.init();
|
||||
});
|
||||
|
||||
describe("reschedule normal booking", () => {
|
||||
it("should create a booking to be rescheduled", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 1, 9, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: normalBookingEventTypeId,
|
||||
attendee: {
|
||||
name: `user-venus-bookings-attendee-${randomString(10)}`,
|
||||
email: `user-venus-bookings-attendee-${randomString(10)}@gmail.com`,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/oj12u83128u9312ou3",
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
expect(bookingResponseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(bookingResponseBody.data)).toBe(true);
|
||||
if (responseDataIsBooking(bookingResponseBody.data)) {
|
||||
const created: BookingOutput_2024_08_13 = bookingResponseBody.data;
|
||||
oldNormalBooking = created;
|
||||
expect(oldNormalBooking).toBeDefined();
|
||||
expect(oldNormalBooking.uid).toBeDefined();
|
||||
} else {
|
||||
throw new Error("should create a booking to be rescheduled - Invalid response data");
|
||||
}
|
||||
});
|
||||
|
||||
it("should reschedule", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 1, 9, 12, 0, 0)).toISOString(),
|
||||
reschedulingReason: RESCHEDULE_REASON,
|
||||
rescheduledBy: RESCHEDULED_BY,
|
||||
};
|
||||
|
||||
const beforeCreate = new Date();
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${oldNormalBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const afterCreate = new Date();
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data as BookingOutput_2024_08_13;
|
||||
expect(data.reschedulingReason).toEqual(RESCHEDULE_REASON);
|
||||
expect(data.rescheduledFromUid).toEqual(oldNormalBooking.uid);
|
||||
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
|
||||
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2040, 1, 9, 13, 0, 0)).toISOString());
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(normalBookingUser.id);
|
||||
expect(data.hosts[0].username).toEqual(normalBookingUser.username);
|
||||
expect(data.hosts[0].email).toEqual(normalBookingUser.email);
|
||||
expect(data.status).toEqual(oldNormalBooking.status);
|
||||
expect(data.duration).toEqual(oldNormalBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(oldNormalBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(oldNormalBooking.attendees[0]);
|
||||
expect(data.location).toEqual(oldNormalBooking.location);
|
||||
expect(data.absentHost).toEqual(oldNormalBooking.absentHost);
|
||||
expect(data.metadata).toEqual(oldNormalBooking.metadata);
|
||||
|
||||
newNormalBooking = data;
|
||||
expect(newNormalBooking).toBeDefined();
|
||||
|
||||
const createdAtDate = new Date(data.createdAt);
|
||||
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch old booking and verify rescheduledToUid and rescheduledByEmail and status cancelled", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${oldNormalBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.rescheduledToUid).toEqual(newNormalBooking.uid);
|
||||
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
|
||||
expect(data.status).toEqual("cancelled");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should fetch new booking and verify rescheduledFromUid, rescheduledByEmail and reschedulingReason", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${newNormalBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.rescheduledFromUid).toEqual(oldNormalBooking.uid);
|
||||
expect(data.rescheduledByEmail).toEqual(RESCHEDULED_BY);
|
||||
expect(data.reschedulingReason).toEqual(RESCHEDULE_REASON);
|
||||
expect(data.status).toEqual("accepted");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(normalBookingUser.email);
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data;
|
||||
}
|
||||
});
|
||||
@@ -64,14 +64,12 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
let team1EventTypeId: number;
|
||||
let team2EventTypeId: number;
|
||||
let team2RREventTypeId: number;
|
||||
let phoneOnlyEventTypeId: number;
|
||||
let collectiveEventWithoutHostsId: number;
|
||||
let roundRobinEventWithoutHostsId: number;
|
||||
|
||||
const team1EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
const team2RREventTypeSlug = `team-bookings-rr-event-type-${randomString()}`;
|
||||
const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
|
||||
let phoneBasedBooking: BookingOutput_2024_08_13;
|
||||
@@ -341,50 +339,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
team2EventTypeId = team2EventType.id;
|
||||
|
||||
const team2RREventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team2.id },
|
||||
},
|
||||
title: `team-bookings-2024-08-13-event-type-rr-${randomString()}`,
|
||||
slug: team2RREventTypeSlug,
|
||||
length: 60,
|
||||
assignAllTeamMembers: true,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
rrHostSubsetEnabled: true,
|
||||
});
|
||||
|
||||
team2RREventTypeId = team2RREventType.id;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team2RREventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team2RREventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
@@ -649,163 +603,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a team 2 RR booking and use rrHostSubsetIds to force teamUser2 as host ", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 12, 0, 0)).toISOString(),
|
||||
eventTypeId: team2RREventTypeId,
|
||||
attendee: {
|
||||
name: "bob2",
|
||||
email: "bob2@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
rrHostSubsetIds: [teamUser2.id],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(teamUser2.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(team2RREventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a team 2 RR booking and use rrHostSubsetIds to force teamUser as host ", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 12, 0, 0)).toISOString(),
|
||||
eventTypeId: team2RREventTypeId,
|
||||
attendee: {
|
||||
name: "bob",
|
||||
email: "bob@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
rrHostSubsetIds: [teamUser.id],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(teamUser.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(team2RREventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
email: body.attendee.email,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a team 2 RR booking and use rrHostSubsetIds to force teamUser and teamUser2 as host ", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
|
||||
eventTypeId: team2RREventTypeId,
|
||||
attendee: {
|
||||
name: "bob",
|
||||
email: "bob@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
rrHostSubsetIds: [teamUser.id, teamUser2.id],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(teamUser.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(team2RREventTypeId);
|
||||
expect(data.attendees.length).toEqual(2);
|
||||
expect(data.attendees.find((a) => a.email === body.attendee.email)).toBeDefined();
|
||||
expect(data.attendees.find((a) => a.email === teamUser2.email)).toBeDefined();
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("get team bookings", () => {
|
||||
@@ -842,7 +639,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
expect(data.length).toEqual(1);
|
||||
expect(data[0].eventTypeId).toEqual(team2EventTypeId);
|
||||
});
|
||||
});
|
||||
@@ -887,7 +684,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(5);
|
||||
expect(data.length).toEqual(3);
|
||||
expect(data.find((booking) => booking.eventTypeId === team1EventTypeId)).toBeDefined();
|
||||
expect(data.find((booking) => booking.eventTypeId === team2EventTypeId)).toBeDefined();
|
||||
});
|
||||
@@ -1048,7 +845,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
await userRepositoryFixture.deleteByEmail(teamUser.email);
|
||||
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser.id, teamUser.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser2.id, teamUser2.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
+356
-351
@@ -4,7 +4,6 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CalVideoService } from "@/ee/bookings/2024-08-13/services/cal-video.service";
|
||||
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
@@ -80,6 +79,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const eventTypeRequiresConfirmationSlug = `user-bookings-event-type-requires-confirmation-${randomString()}`;
|
||||
|
||||
let createdBooking: BookingOutput_2024_08_13;
|
||||
let rescheduledBooking: BookingOutput_2024_08_13;
|
||||
let createdRecurringBooking: RecurringBookingOutput_2024_08_13[];
|
||||
|
||||
let bookingInThePast: Booking;
|
||||
@@ -1360,217 +1360,139 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rescheduling bookings", () => {
|
||||
describe("cant't reschedule cancelled or rescheduled booking booking", () => {
|
||||
it("should not be able to reschedule cancelled booking", async () => {
|
||||
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||
status: "CANCELLED",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `cancelled-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
describe("reschedule bookings", () => {
|
||||
it("should reschedule normal booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const beforeCreate = new Date();
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const afterCreate = new Date();
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 15, 0, 0)).toISOString());
|
||||
expect(data.rescheduledFromUid).toEqual(createdBooking.uid);
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual(createdBooking.status);
|
||||
expect(data.duration).toEqual(createdBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
|
||||
expect(data.location).toEqual(createdBooking.location);
|
||||
expect(data.absentHost).toEqual(createdBooking.absentHost);
|
||||
expect(data.metadata).toEqual(createdBooking.metadata);
|
||||
|
||||
// When a booking is rescheduled, a new booking is created and the old booking is cancelled.
|
||||
// We want to make sure the createdAt date of the new booking is between the beforeCreate and afterCreate dates.
|
||||
const createdAtDate = new Date(data.createdAt);
|
||||
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
|
||||
rescheduledBooking = data;
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
|
||||
});
|
||||
|
||||
it("should not be able to reschedule rescheduled booking", async () => {
|
||||
const rescheduledBooking = await bookingsRepositoryFixture.create({
|
||||
status: "CANCELLED",
|
||||
rescheduled: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `cancelled-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newBooking = await bookingsRepositoryFixture.create({
|
||||
status: "ACCEPTED",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
fromReschedule: rescheduledBooking.uid,
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `new-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
|
||||
await bookingsRepositoryFixture.deleteById(newBooking.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recurring booking", () => {
|
||||
it("should reschedule recurrence of a recurring booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars again",
|
||||
};
|
||||
it("should set rescheduled booking status to cancelled", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.status).toEqual("cancelled");
|
||||
expect(data.rescheduledToUid).toEqual(rescheduledBooking.uid);
|
||||
});
|
||||
});
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual(createdRecurringBooking[0].status);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(createdRecurringBooking[0].duration);
|
||||
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
|
||||
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
|
||||
expect(data.location).toEqual(createdRecurringBooking[0].location);
|
||||
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
|
||||
it("should reschedule recurrence of a recurring booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars again",
|
||||
};
|
||||
|
||||
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
|
||||
expect(oldBooking).toBeDefined();
|
||||
expect(oldBooking?.status).toEqual("CANCELLED");
|
||||
});
|
||||
});
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdRecurringBooking[0].uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: RecurringBookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual(createdRecurringBooking[0].status);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(createdRecurringBooking[0].duration);
|
||||
expect(data.recurringBookingUid).toEqual(createdRecurringBooking[0].recurringBookingUid);
|
||||
expect(data.eventTypeId).toEqual(createdRecurringBooking[0].eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
|
||||
expect(data.location).toEqual(createdRecurringBooking[0].location);
|
||||
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
|
||||
|
||||
it("should get recurring booking recurrences after rescheduling one", async () => {
|
||||
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${recurringBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
|
||||
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
|
||||
expect(oldBooking).toBeDefined();
|
||||
expect(oldBooking?.status).toEqual("CANCELLED");
|
||||
});
|
||||
});
|
||||
|
||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(4);
|
||||
const cancelled = data.find((booking) => booking.status === "cancelled");
|
||||
expect(cancelled).toBeDefined();
|
||||
const rescheduledNew = data.find(
|
||||
(booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString()
|
||||
);
|
||||
expect(rescheduledNew).toBeDefined();
|
||||
createdRecurringBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
it("should get recurring booking recurrences after rescheduling one", async () => {
|
||||
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${recurringBookingUid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsRecurringBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||
const data: RecurringBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(4);
|
||||
const cancelled = data.find((booking) => booking.status === "cancelled");
|
||||
expect(cancelled).toBeDefined();
|
||||
const rescheduledNew = data.find(
|
||||
(booking) => booking.start === new Date(Date.UTC(2035, 0, 9, 14, 0, 0)).toISOString()
|
||||
);
|
||||
expect(rescheduledNew).toBeDefined();
|
||||
createdRecurringBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2463,6 +2385,95 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rescheduling booking with invalid booking responses", () => {
|
||||
it("should reschedule", async () => {
|
||||
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
|
||||
const username = `user-bookings-attendee-${randomString(10)}`;
|
||||
const locale = "it";
|
||||
const timeZone = "Europe/Rome";
|
||||
const attendee = await userRepositoryFixture.create({
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${randomString(10)}`,
|
||||
title: "booking title",
|
||||
startTime: "2060-09-05T11:00:00.000Z",
|
||||
endTime: "2060-09-05T12:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: email,
|
||||
name: username,
|
||||
timeZone,
|
||||
locale,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${booking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.rescheduledFromUid).toEqual(booking.uid);
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.eventTypeId).toEqual(booking.eventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.bookingFieldsResponses).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
rescheduledReason: body.reschedulingReason,
|
||||
guests: [],
|
||||
});
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
await userRepositoryFixture.delete(attendee.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("create booking with attendee sms reminder", () => {
|
||||
it("should not be able create a booking if attendee sms reminder is missing", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
@@ -2548,6 +2559,146 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cant't reschedule cancelled booking", () => {
|
||||
it("should not be able to reschedule cancelled booking", async () => {
|
||||
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||
status: "CANCELLED",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `cancelled-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${cancelledBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Can't reschedule booking with uid=${cancelledBooking.uid} because it has been cancelled. Please provide uid of a booking that is not cancelled.`
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteById(cancelledBooking.id);
|
||||
});
|
||||
|
||||
it("should not be able to reschedule rescheduled booking", async () => {
|
||||
const rescheduledBooking = await bookingsRepositoryFixture.create({
|
||||
status: "CANCELLED",
|
||||
rescheduled: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `cancelled-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newBooking = await bookingsRepositoryFixture.create({
|
||||
status: "ACCEPTED",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
fromReschedule: rescheduledBooking.uid,
|
||||
startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: `new-booking-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "integrations:daily",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Oldie",
|
||||
email: "oldie@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "oldie@gmail.com",
|
||||
name: "Oldie",
|
||||
locale: "lv",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toEqual(
|
||||
`Can't reschedule booking with uid=${rescheduledBooking.uid} because it has been cancelled and rescheduled already to booking with uid=${newBooking.uid}. You probably want to reschedule ${newBooking.uid} instead by passing it within the request URL.`
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteById(rescheduledBooking.id);
|
||||
await bookingsRepositoryFixture.deleteById(newBooking.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cant't cancel already cancelled booking", () => {
|
||||
it("should not be able to cancel alraedy cancelled booking", async () => {
|
||||
const cancelledBooking = await bookingsRepositoryFixture.create({
|
||||
@@ -2601,56 +2752,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancellation reason and cancelled by returned if null in database", () => {
|
||||
it("should return cancellation reason and cancelled by if null in database", async () => {
|
||||
const dbBooking = await bookingsRepositoryFixture.create({
|
||||
uid: randomString(10),
|
||||
title: "test",
|
||||
startTime: new Date(Date.UTC(2040, 0, 13, 16, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2040, 0, 13, 17, 0, 0)),
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "tester",
|
||||
email: "tester@example.com",
|
||||
guests: [],
|
||||
},
|
||||
cancellationReason: null,
|
||||
cancelledBy: null,
|
||||
});
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${dbBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.cancellationReason).toBe("");
|
||||
expect(data.cancelledByEmail).toBe("");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("calendar events", () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
@@ -3079,102 +3180,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
);
|
||||
}
|
||||
|
||||
describe("Meeting sessions", () => {
|
||||
it("should get cal video sessions for a booking", async () => {
|
||||
const mockSessions = [
|
||||
{
|
||||
id: "session-123",
|
||||
room: "daily-room-123",
|
||||
start_time: 1678901234,
|
||||
duration: 3600,
|
||||
ongoing: false,
|
||||
max_participants: 10,
|
||||
participants: [
|
||||
{
|
||||
user_id: "user-1",
|
||||
participant_id: "participant-1",
|
||||
user_name: "John Doe",
|
||||
join_time: 1678901234,
|
||||
duration: 3600,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `test-video-session-${randomString()}`,
|
||||
title: "Test Video Session Booking",
|
||||
description: "",
|
||||
startTime: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)),
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
references: {
|
||||
create: [
|
||||
{
|
||||
type: "daily_video",
|
||||
uid: `daily-room-123-${randomString()}`,
|
||||
meetingId: "daily-room-123",
|
||||
meetingPassword: "test-password",
|
||||
meetingUrl: "https://daily.co/daily-room-123",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const calVideoService = app.get(CalVideoService);
|
||||
jest
|
||||
.spyOn(calVideoService, "getVideoSessions")
|
||||
.mockResolvedValue([
|
||||
{
|
||||
id: mockSessions[0].id,
|
||||
room: mockSessions[0].room,
|
||||
startTime: mockSessions[0].start_time,
|
||||
duration: mockSessions[0].duration,
|
||||
ongoing: mockSessions[0].ongoing,
|
||||
maxParticipants: mockSessions[0].max_participants,
|
||||
participants: mockSessions[0].participants.map((p) => ({
|
||||
userId: p.user_id,
|
||||
userName: p.user_name,
|
||||
joinTime: p.join_time,
|
||||
duration: p.duration,
|
||||
})),
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${booking.uid}/conferencing-sessions`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.status).toEqual(SUCCESS_STATUS);
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(Array.isArray(response.body.data)).toBe(true);
|
||||
expect(response.body.data.length).toBe(1);
|
||||
expect(response.body.data[0].id).toEqual(mockSessions[0].id);
|
||||
expect(response.body.data[0].room).toEqual(mockSessions[0].room);
|
||||
expect(response.body.data[0].startTime).toEqual(mockSessions[0].start_time);
|
||||
expect(response.body.data[0].duration).toEqual(mockSessions[0].duration);
|
||||
expect(response.body.data[0].ongoing).toEqual(mockSessions[0].ongoing);
|
||||
expect(response.body.data[0].maxParticipants).toEqual(mockSessions[0].max_participants);
|
||||
expect(response.body.data[0].participants.length).toBe(1);
|
||||
expect(response.body.data[0].participants[0].userId).toEqual(mockSessions[0].participants[0].user_id);
|
||||
expect(response.body.data[0].participants[0].userName).toEqual(
|
||||
mockSessions[0].participants[0].user_name
|
||||
);
|
||||
|
||||
await bookingsRepositoryFixture.deleteById(booking.id);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { ApiAuthGuardUser } from "@/modules/auth/strategies/api-auth/api-auth.strategy";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
UnauthorizedException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { BookingAccessService } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class BookingPbacGuard implements CanActivate {
|
||||
private bookingAccessService: BookingAccessService;
|
||||
|
||||
constructor(private readonly prismaReadService: PrismaReadService) {
|
||||
this.bookingAccessService = new BookingAccessService(this.prismaReadService.prisma);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context
|
||||
.switchToHttp()
|
||||
.getRequest<Request & { user?: ApiAuthGuardUser; pbacAuthorizedRequest?: boolean }>();
|
||||
const user = request.user;
|
||||
const bookingUid = request.params.bookingUid;
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
if (!bookingUid) {
|
||||
throw new BadRequestException("BookingPbacGuard - bookingUid is required");
|
||||
}
|
||||
|
||||
const hasAccess = await this.bookingAccessService.doesUserIdHaveAccessToBooking({
|
||||
userId: user.id,
|
||||
bookingUid,
|
||||
});
|
||||
|
||||
if (!hasAccess) {
|
||||
throw new ForbiddenException(
|
||||
`BookingPbacGuard - user with id=${user.id} does not have access to booking with uid=${bookingUid}`
|
||||
);
|
||||
}
|
||||
|
||||
request.pbacAuthorizedRequest = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user