Compare commits

..

No commits in common. "develop" and "v1.1.0" have entirely different histories.

160 changed files with 23632 additions and 11678 deletions

View File

@ -1,3 +1 @@
{ { "extends": ["@commitlint/config-conventional"] }
"extends": ["@commitlint/config-conventional"]
}

View File

@ -1 +1,2 @@
FROM mcr.microsoft.com/devcontainers/javascript-node:20 ARG VARIANT="16"
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

View File

@ -1,23 +0,0 @@
services:
workspace:
build:
context: "./"
dockerfile: "./Dockerfile"
volumes:
- "..:/workspace:cached"
command: "sleep infinity"
network_mode: "host"
thream-database:
image: "postgres:15.4"
environment:
POSTGRES_USER: "thream_user"
POSTGRES_PASSWORD: "password"
POSTGRES_DB: "thream"
volumes:
- "thream-postgres-data:/var/lib/postgresql/data"
restart: "unless-stopped"
network_mode: "host"
volumes:
thream-postgres-data:

View File

@ -1,14 +1,10 @@
{ {
"name": "@thream/api", "name": "@thream/api",
"dockerComposeFile": "./compose.yaml", "dockerComposeFile": "./docker-compose.yml",
"service": "workspace", "service": "workspace",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"settings": { "settings": {
"remote.autoForwardPorts": false, "remote.autoForwardPorts": false
"remote.localPortHost": "allInterfaces"
}
}, },
"extensions": [ "extensions": [
"editorconfig.editorconfig", "editorconfig.editorconfig",
@ -18,7 +14,8 @@
"prisma.prisma", "prisma.prisma",
"mikestead.dotenv", "mikestead.dotenv",
"ms-azuretools.vscode-docker" "ms-azuretools.vscode-docker"
] ],
}, "forwardPorts": [8080, 5555, 5432, 1080],
"postAttachCommand": ["npm", "install"],
"remoteUser": "node" "remoteUser": "node"
} }

View File

@ -0,0 +1,30 @@
version: '3.0'
services:
workspace:
build:
context: './'
dockerfile: './Dockerfile'
volumes:
- '..:/workspace:cached'
command: 'sleep infinity'
extra_hosts:
- 'host.docker.internal:host-gateway'
thream-database:
image: 'postgres:14.2'
environment:
POSTGRES_USER: 'user'
POSTGRES_PASSWORD: 'password'
POSTGRES_DB: 'thream'
volumes:
- 'postgres-data:/var/lib/postgresql/data'
restart: 'unless-stopped'
thream-maildev:
image: 'maildev/maildev:1.1.0'
ports:
- '1080:80'
volumes:
postgres-data:

View File

@ -1,6 +1,9 @@
.* .vscode
!.npmrc .git
!.swcrc .env
build build
coverage coverage
.nyc_output
node_modules node_modules
tmp
temp

View File

@ -1,29 +1,20 @@
API_URL=http://127.0.0.1:8080 API_URL=http://localhost:8080
COMPOSE_PROJECT_NAME=thream-api COMPOSE_PROJECT_NAME=thream-api
DATABASE_URL=postgresql://user:password@thream-database:5432/thream
DATABASE_USER=thream_user
DATABASE_PASSWORD=password
DATABASE_NAME=thream
DATABASE_URL=postgresql://thream_user:password@127.0.0.1:5432/thream
EMAIL_HOST=0.0.0.0
EMAIL_PASSWORD=password
EMAIL_PORT=1025
EMAIL_USER=no-reply@thream.fr
DISCORD_CLIENT_ID= DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET= DISCORD_CLIENT_SECRET=
EMAIL_HOST=thream-maildev
EMAIL_PASSWORD=password
EMAIL_PORT=25
EMAIL_USER=no-reply@thream.fr
FILE_UPLOADS_API_KEY=apiKeySecret FILE_UPLOADS_API_KEY=apiKeySecret
FILE_UPLOADS_API_URL=http://127.0.0.1:8000 FILE_UPLOADS_API_URL=http://host.docker.internal:8000
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET= GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=
HOST=0.0.0.0 HOST=0.0.0.0
JWT_ACCESS_EXPIRES_IN=15 minutes JWT_ACCESS_EXPIRES_IN=15 minutes
# You can generate JWT secrets with the `npm run generate:jwt-secret` command.
JWT_ACCESS_SECRET=accessTokenSecret JWT_ACCESS_SECRET=accessTokenSecret
JWT_REFRESH_SECRET=refreshTokenSecret JWT_REFRESH_SECRET=refreshTokenSecret
NODE_ENV=development NODE_ENV=development

View File

@ -1,13 +1,16 @@
{ {
"extends": ["conventions", "prettier"], "extends": ["conventions", "prettier"],
"plugins": ["prettier", "import", "unicorn"], "plugins": ["prettier", "import", "unicorn"],
"parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
"env": {
"node": true
},
"rules": { "rules": {
"prettier/prettier": "error", "prettier/prettier": "error",
"import/extensions": ["error", "always"], "import/extensions": ["error", "always"],
"unicorn/prevent-abbreviations": "error" "unicorn/prevent-abbreviations": "error",
"unicorn/prefer-node-protocol": "error"
} }
} }

View File

@ -1,8 +1,8 @@
--- ---
name: "🐛 Bug Report" name: '🐛 Bug Report'
about: "Report an unexpected problem or unintended behavior." about: 'Report an unexpected problem or unintended behavior.'
title: "[Bug]" title: '[Bug]'
labels: "bug" labels: 'bug'
--- ---
<!-- <!--

View File

@ -1,8 +1,8 @@
--- ---
name: "📜 Documentation" name: '📜 Documentation'
about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)." about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).'
title: "[Documentation]" title: '[Documentation]'
labels: "documentation" labels: 'documentation'
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: "✨ Feature Request" name: '✨ Feature Request'
about: "Suggest a new feature idea." about: 'Suggest a new feature idea.'
title: "[Feature]" title: '[Feature]'
labels: "feature request" labels: 'feature request'
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: "🔧 Improvement" name: '🔧 Improvement'
about: "Improve structure/format/performance/refactor/tests of the code." about: 'Improve structure/format/performance/refactor/tests of the code.'
title: "[Improvement]" title: '[Improvement]'
labels: "improvement" labels: 'improvement'
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: "🙋 Question" name: '🙋 Question'
about: "Further information is requested." about: 'Further information is requested.'
title: "[Question]" title: '[Question]'
labels: "question" labels: 'question'
--- ---
### Question ### Question

View File

@ -1,6 +1,6 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. --> <!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
# What changes this PR introduce? ## What changes this PR introduce?
## List any relevant issue numbers ## List any relevant issue numbers

View File

@ -1,4 +1,4 @@
name: "Analyze" name: 'Analyze'
on: on:
push: push:
@ -8,20 +8,20 @@ on:
jobs: jobs:
analyze: analyze:
runs-on: "ubuntu-latest" runs-on: 'ubuntu-latest'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: ["javascript"] language: ['javascript']
steps: steps:
- uses: "actions/checkout@v4.0.0" - uses: 'actions/checkout@v3.0.0'
- name: "Initialize CodeQL" - name: 'Initialize CodeQL'
uses: "github/codeql-action/init@v2" uses: 'github/codeql-action/init@v1'
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: "Perform CodeQL Analysis" - name: 'Perform CodeQL Analysis'
uses: "github/codeql-action/analyze@v1" uses: 'github/codeql-action/analyze@v1'

View File

@ -1,4 +1,4 @@
name: "Build" name: 'Build'
on: on:
push: push:
@ -8,20 +8,18 @@ on:
jobs: jobs:
build: build:
runs-on: "ubuntu-latest" runs-on: 'ubuntu-latest'
steps: steps:
- uses: "actions/checkout@v4.0.0" - uses: 'actions/checkout@v3.0.0'
- name: "Setup Node.js" - name: 'Use Node.js'
uses: "actions/setup-node@v3.8.1" uses: 'actions/setup-node@v3.0.0'
with: with:
node-version: "20.x" node-version: '16.x'
cache: "npm" cache: 'npm'
- name: "Install dependencies" - name: 'Install'
run: "npm clean-install" run: 'npm install'
- name: "Build" - name: 'Build'
run: "npm run build" run: 'npm run build'
- run: "npm run build:typescript"

View File

@ -1,4 +1,4 @@
name: "Lint" name: 'Lint'
on: on:
push: push:
@ -8,33 +8,40 @@ on:
jobs: jobs:
lint: lint:
runs-on: "ubuntu-latest" runs-on: 'ubuntu-latest'
steps: steps:
- uses: "actions/checkout@v4.0.0" - uses: 'actions/checkout@v3.0.0'
- name: "Setup Node.js" - name: 'Use Node.js'
uses: "actions/setup-node@v3.8.1" uses: 'actions/setup-node@v3.0.0'
with: with:
node-version: "20.x" node-version: '16.x'
cache: "npm" cache: 'npm'
- name: "Install dependencies" - name: 'Install'
run: "npm clean-install" run: 'npm install'
- name: "lint:commit" - name: 'lint:commit'
run: 'npm run lint:commit -- --to "${{ github.sha }}"' run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- name: "lint:editorconfig" - name: 'lint:editorconfig'
run: "npm run lint:editorconfig" run: 'npm run lint:editorconfig'
- name: "lint:markdown" - name: 'lint:markdown'
run: "npm run lint:markdown" run: 'npm run lint:markdown'
- name: "lint:eslint" - name: 'lint:typescript'
run: "npm run lint:eslint" run: 'npm run lint:typescript'
- name: "lint:prettier" - name: 'lint:prettier'
run: "npm run lint:prettier" run: 'npm run lint:prettier'
- name: "prisma:validate" - name: 'lint:dotenv'
run: "cp .env.example .env && npm run prisma:validate" uses: 'dotenv-linter/action-dotenv-linter@v2'
with:
github_token: ${{ secrets.github_token }}
- name: 'lint:docker'
uses: 'hadolint/hadolint-action@v1.6.0'
with:
dockerfile: './Dockerfile'

View File

@ -1,4 +1,4 @@
name: "Release" name: 'Release'
on: on:
push: push:
@ -6,36 +6,34 @@ on:
jobs: jobs:
release: release:
runs-on: "ubuntu-latest" runs-on: 'ubuntu-latest'
steps: steps:
- uses: "actions/checkout@v4.0.0" - uses: 'actions/checkout@v3.0.0'
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: "Import GPG key" - name: 'Import GPG key'
uses: "crazy-max/ghaction-import-gpg@v6.0.0" uses: 'crazy-max/ghaction-import-gpg@v3.2.0'
with: with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
git_user_signingkey: true git-user-signingkey: true
git_commit_gpgsign: true git-commit-gpgsign: true
- name: "Setup Node.js" - name: 'Use Node.js'
uses: "actions/setup-node@v3.8.1" uses: 'actions/setup-node@v3.0.0'
with: with:
node-version: "20.x" node-version: '16.x'
cache: "npm" cache: 'npm'
- name: "Install dependencies" - name: 'Install'
run: "npm clean-install" run: 'npm install'
- name: "Build" - name: 'Build'
run: "npm run build" run: 'npm run build'
- run: "npm run build:typescript" - name: 'Release'
run: 'npm run release'
- name: "Release"
run: "npm run release"
env: env:
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }} GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}

View File

@ -1,4 +1,4 @@
name: "Test" name: 'Test'
on: on:
push: push:
@ -8,23 +8,23 @@ on:
jobs: jobs:
test: test:
runs-on: "ubuntu-latest" runs-on: 'macos-latest'
steps: steps:
- uses: "actions/checkout@v4.0.0" - uses: 'actions/checkout@v3.0.0'
- name: "Setup Node.js" - name: 'Use Node.js'
uses: "actions/setup-node@v3.8.1" uses: 'actions/setup-node@v3.0.0'
with: with:
node-version: "20.x" node-version: '16.x'
cache: "npm" cache: 'npm'
- name: "Install dependencies" - name: 'Install'
run: "npm clean-install" run: 'npm install'
- name: "Build" - name: 'Build'
run: "npm run build" run: 'npm run build'
- run: "cp .env.example .env" - run: 'cp .env.example .env'
- name: "Test" - name: 'Test'
run: "npm run test" run: 'npm run test'

1
.gitignore vendored
View File

@ -35,4 +35,3 @@ npm-debug.log*
# misc # misc
.DS_Store .DS_Store
*.hbs

View File

@ -3,4 +3,3 @@
npm run lint:staged npm run lint:staged
npm run build npm run build
npm run build:typescript

View File

@ -2,6 +2,5 @@
"*": ["editorconfig-checker"], "*": ["editorconfig-checker"],
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"], "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
"*.{json,jsonc,yml,yaml}": ["prettier --write"], "*.{json,jsonc,yml,yaml}": ["prettier --write"],
"*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"], "*.md": ["prettier --write", "markdownlint --dot --fix"]
"prisma/schema.prisma": ["prisma validate"]
} }

View File

@ -1,11 +0,0 @@
{
"config": {
"extends": "markdownlint/style/prettier",
"relative-links": true,
"default": true,
"MD033": false
},
"globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"]
}

6
.markdownlint.json Normal file
View File

@ -0,0 +1,6 @@
{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
}

5
.nycrc.json Normal file
View File

@ -0,0 +1,5 @@
{
"reporter": ["text", "cobertura"],
"src": "./build",
"all": true
}

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
build
node_modules
coverage
package.json
package-lock.json
*.hbs

View File

@ -1,3 +1,6 @@
{ {
"semi": false "singleQuote": true,
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
} }

15
.swcrc
View File

@ -1,13 +1,22 @@
{ {
"sourceMaps": true,
"jsc": { "jsc": {
"parser": { "parser": {
"syntax": "typescript", "syntax": "typescript",
"decorators": true,
"dynamicImport": true "dynamicImport": true
}, },
"target": "esnext" "transform": {
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2022",
"loose": true
}, },
"module": { "module": {
"type": "es6" "type": "es6",
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
} }
} }

8
.taprc Normal file
View File

@ -0,0 +1,8 @@
ts: false
jsx: false
flow: false
check-coverage: false
coverage: false
files:
- 'build/**/*.test.js'

View File

@ -5,7 +5,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit" "source.fixAll": true
}, },
"eslint.options": { "ignorePath": ".gitignore" }, "eslint.options": { "ignorePath": ".gitignore" },
"[prisma]": { "[prisma]": {

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
<contact@theoludwig.fr>. contact@divlo.fr.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@ -29,14 +29,38 @@ If you're adding new features to **Thream/api**, please include tests.
## Commits ## Commits
The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases. The commit message guidelines respect
[@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)
and [Semantic Versioning](https://semver.org/) for releases.
### Types
Types define which kind of changes you made to the project.
| Types | Description |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| feat | A new feature. |
| fix | A bug fix. |
| docs | Documentation only changes. |
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
| refactor | A code change that neither fixes a bug nor adds a feature. |
| perf | A code change that improves performance. |
| test | Adding missing tests or correcting existing tests. |
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
| chore | Other changes that don't modify src or test files. |
| revert | Reverts a previous commit. |
### Scopes
Scopes define what part of the code changed.
### Examples ### Examples
```sh ```sh
git commit -m "feat: add POST /users/signup" git commit -m "feat(services): add POST /users/signup"
git commit -m "docs(readme): update installation process" git commit -m "docs(readme): update installation process"
git commit -m "fix: should emit events to connected users" git commit -m "fix(services): should emit events to connected users"
``` ```
## Directory Structure ## Directory Structure
@ -62,6 +86,7 @@ git commit -m "fix: should emit events to connected users"
- `services` : all REST API endpoints - `services` : all REST API endpoints
- `tools` : configs and utilities - `tools` : configs and utilities
- `typings` : types gloablly used in the project - `typings` : types gloablly used in the project
- `uploads` : uploaded files by users
### Services folder explained with an example ### Services folder explained with an example
@ -96,5 +121,3 @@ The folders after `src/services` : is the real path of the routes in the API exc
folders starting and ending with `__` like `__test__` or `__utils__`. folders starting and ending with `__` like `__test__` or `__utils__`.
The filenames correspond to the HTTP methods used (`get`, `post`, `put`, `delete`). The filenames correspond to the HTTP methods used (`get`, `post`, `put`, `delete`).
You can generate the boilerplate code for a new service with the `npm run generate` command.

View File

@ -1,22 +1,23 @@
FROM node:20.9.0 AS dependencies FROM node:16.14.2 AS dependencies
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY ./package*.json ./ COPY ./package*.json ./
RUN npm clean-install RUN npm install
FROM node:20.9.0 AS builder FROM node:16.14.2 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=dependencies /usr/src/app/node_modules ./node_modules COPY --from=dependencies /usr/src/app/node_modules ./node_modules
COPY ./ ./ COPY ./ ./
RUN npm run prisma:generate && npm run build RUN npm run prisma:generate && npm run build
FROM node:20.9.0 AS runner FROM node:16.14.2 AS runner
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NODE_OPTIONS=--enable-source-maps
COPY --from=builder /usr/src/app/node_modules ./node_modules COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/start.sh ./docker-start.sh
COPY --from=builder /usr/src/app/package.json ./package.json COPY --from=builder /usr/src/app/package.json ./package.json
COPY --from=builder /usr/src/app/email ./email COPY --from=builder /usr/src/app/email ./email
COPY --from=builder /usr/src/app/build ./build COPY --from=builder /usr/src/app/build ./build
COPY --from=builder /usr/src/app/prisma ./prisma COPY --from=builder /usr/src/app/prisma ./prisma
COPY --from=builder /usr/src/app/uploads ./uploads
USER node USER node
CMD npm run prisma:migrate:deploy && node build/index.js CMD ["./docker-start.sh"]

View File

@ -1,8 +1,4 @@
<h1 align="center"><a href="https://api.thream.theoludwig.fr/documentation">Thream/api</a></h1> <h1 align="center"><a href="https://api.thream.divlo.fr/documentation">Thream/api</a></h1>
<p align="center">
<strong>⚠️ This project is not maintained anymore, you can still use the code as you wish and fork it to maintain it yourself.</strong>
</p>
<p align="center"> <p align="center">
<a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a> <a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
@ -22,7 +18,7 @@
Thream's Application Programming Interface (API) to stay close with your friends and communities. Thream's Application Programming Interface (API) to stay close with your friends and communities.
It uses [Thream/file-uploads-api](https://github.com/Thream/file-uploads-api) [v1.1.8](https://github.com/Thream/file-uploads-api/releases/tag/v1.1.8). It uses [Thream/file-uploads-api](https://github.com/Thream/file-uploads-api) [v1.1.0](https://github.com/Thream/file-uploads-api/releases/tag/v1.1.0).
## ⚙️ Getting Started ## ⚙️ Getting Started
@ -36,81 +32,61 @@ It uses [Thream/file-uploads-api](https://github.com/Thream/file-uploads-api) [v
```sh ```sh
# Clone the repository # Clone the repository
git clone git@github.com:Thream/api.git git clone https://github.com/Thream/api.git
# Go to the project root # Go to the project root
cd api cd api
# Install dependencies
npm clean-install
# Configure environment variables # Configure environment variables
cp .env.example .env cp .env.example .env
# Generate Prisma client types # Install
npm run prisma:generate npm install
``` ```
### Database Setup You will need to configure the environment variables by creating an `.env` file at
the root of the project (see `.env.example`).
### Local Development environment
#### Setup the database
```sh ```sh
# Create a new user and database # Create a new user and database
psql psql
CREATE DATABASE thream; create database thream_database;
CREATE USER thream_user with encrypted password 'password'; create user thream_user with encrypted password 'password';
ALTER USER thream_user WITH SUPERUSER; ALTER USER thream_user WITH SUPERUSER;
``` ```
### Database Production migration Replace `DATABASE_URL` inside `.env` with `postgresql://thream_user:password@localhost:5432/thream_database`
```sh
npm run prisma:migrate:deploy
```
### Local Development environment
Recommended to use [VSCode: Remote development in Containers](https://code.visualstudio.com/docs/remote/containers-tutorial).
#### Database Development migration
```sh ```sh
# Run Prisma migrations # Run Prisma migrations
npm run prisma:migrate:dev npm run prisma:migrate:dev
# Reset the database (WARNING: This will delete all data)
npm run prisma:migrate:reset
``` ```
#### Usage #### Usage
```sh ```sh
# Run API
npm run dev npm run dev
# Run Prisma Studio
npm run prisma:studio
``` ```
##### Services started ### Production environment with [Docker](https://www.docker.com/)
- `api`: <http://127.0.0.1:8080>
- [Maildev](https://maildev.github.io/maildev/): <http://127.0.0.1:1080>
- [Prisma Studio](https://www.prisma.io/studio): <http://127.0.0.1:5555>
##### Commands
```sh ```sh
# Build, Lint and Test # Setup and run all the services for you
npm run build docker-compose up --build
npm run build:typescript
npm run lint:editorconfig
npm run lint:markdown
npm run lint:eslint
npm run lint:prettier
npm run test
``` ```
### Production environment (with [Docker](https://www.docker.com/)) #### Services started
```sh - API : `http://localhost:8080`
docker compose up --build - [PostgreSQL database](https://www.postgresql.org/)
```
## 💡 Contributing ## 💡 Contributing

View File

@ -1,27 +0,0 @@
services:
thream-api:
container_name: "thream-api"
image: "thream-api"
restart: "unless-stopped"
network_mode: "host"
build:
context: "./"
env_file: ".env"
depends_on:
- "thream-database"
thream-database:
container_name: "thream-database"
image: "postgres:15.4"
restart: "unless-stopped"
network_mode: "host"
env_file: ".env"
environment:
POSTGRES_USER: ${DATABASE_USER}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
volumes:
- "thream-postgres-data:/var/lib/postgresql/data"
volumes:
thream-postgres-data:

29
docker-compose.yml Normal file
View File

@ -0,0 +1,29 @@
version: '3.0'
services:
thream-api:
container_name: ${COMPOSE_PROJECT_NAME}
build:
context: './'
env_file:
- '.env'
ports:
- '${PORT}:${PORT}'
depends_on:
- 'thream-database'
volumes:
- './uploads:/usr/src/app/uploads'
restart: 'unless-stopped'
thream-database:
container_name: 'thream-database'
image: 'postgres:14.2'
environment:
POSTGRES_USER: 'user'
POSTGRES_PASSWORD: 'password'
POSTGRES_DB: 'thream'
volumes:
- 'database-volume:/var/lib/postgresql/data'
restart: 'unless-stopped'
volumes:
database-volume:

4
docker-start.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/bash
npm run prisma:migrate:deploy
node build/index.js

View File

@ -1,45 +1,45 @@
/** @type {import('node-plop').PlopGeneratorConfig} */ /** @type {import('node-plop').PlopGeneratorConfig} */
export const serviceGenerator = { export const serviceGenerator = {
description: "REST API endpoint", description: 'REST API endpoint',
prompts: [ prompts: [
{ {
type: "input", type: 'input',
name: "url", name: 'url',
message: "url", message: 'url'
}, },
{ {
type: "list", type: 'list',
name: "httpMethod", name: 'httpMethod',
message: "httpMethod", message: 'httpMethod',
choices: ["GET", "POST", "PUT", "DELETE"], choices: ['GET', 'POST', 'PUT', 'DELETE']
}, },
{ {
type: "input", type: 'input',
name: "description", name: 'description',
message: "description", message: 'description'
}, },
{ {
type: "list", type: 'list',
name: "tag", name: 'tag',
message: "tag", message: 'tag',
choices: ["users", "oauth2", "guilds", "channels", "messages", "members"], choices: ['users', 'guilds', 'channels', 'messages', 'members', 'uploads']
}, },
{ {
type: "confirm", type: 'confirm',
name: "shouldBeAuthenticated", name: 'shouldBeAuthenticated',
message: "shouldBeAuthenticated", message: 'shouldBeAuthenticated'
}, }
], ],
actions: [ actions: [
{ {
type: "add", type: 'add',
path: "src/services/{{url}}/{{lowerCase httpMethod}}.ts", path: 'src/services/{{url}}/{{lowerCase httpMethod}}.ts',
templateFile: "generators/service/service.ts.hbs", templateFile: 'generators/service/service.ts.hbs'
}, },
{ {
type: "add", type: 'add',
path: "src/services/{{url}}/__test__/{{lowerCase httpMethod}}.test.ts", path: 'src/services/{{url}}/__test__/{{lowerCase httpMethod}}.test.ts',
templateFile: "generators/service/service.test.ts.hbs", templateFile: 'generators/service/service.test.ts.hbs'
}, }
], ]
} }

View File

@ -1,20 +1,18 @@
import test from 'node:test' import tap from 'tap'
import assert from 'node:assert/strict'
import sinon from 'sinon' import sinon from 'sinon'
import { application } from '#src/application.js' import { application } from 'application.js'
{{#if shouldBeAuthenticated}} {{#if shouldBeAuthenticated}}
import { authenticateUserTest } from '#src/__test__/utils/authenticateUserTest.js' import { authenticateUserTest } from '__test__/utils/authenticateUserTest.js'
{{/if}} {{/if}}
import prisma from '#src/tools/database/prisma.js' import prisma from 'tools/database/prisma.js'
await test('{{httpMethod}} {{url}}', async (t) => { await tap.test('{{httpMethod}} {{url}}', async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async () => { await t.test('succeeds', async (t) => {
{{#if shouldBeAuthenticated}} {{#if shouldBeAuthenticated}}
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
{{/if}} {{/if}}
@ -34,6 +32,6 @@ await test('{{httpMethod}} {{url}}', async (t) => {
payload: {} payload: {}
}) })
// const responseJson = response.json() // const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
}) })
}) })

View File

@ -1,10 +1,10 @@
import { Static, Type } from '@sinclair/typebox' import { Static, Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { FastifyPluginAsync, FastifySchema } from 'fastify'
import prisma from '#src/tools/database/prisma.js' import prisma from 'tools/database/prisma.js'
import { fastifyErrors } from '#src/models/utils.js' import { fastifyErrors } from 'models/utils.js'
{{#if shouldBeAuthenticated}} {{#if shouldBeAuthenticated}}
import authenticateUser from '#src/tools/plugins/authenticateUser.js' import authenticateUser from 'tools/plugins/authenticateUser.js'
{{/if}} {{/if}}
const body{{sentenceCase httpMethod}}ServiceSchema = Type.Object({ const body{{sentenceCase httpMethod}}ServiceSchema = Type.Object({

27983
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,110 +1,101 @@
{ {
"name": "@thream/api", "name": "@thream/api",
"version": "1.2.10", "version": "1.1.0",
"description": "Thream's application programming interface to stay close with your friends and communities.", "description": "Thream's application programming interface to stay close with your friends and communities.",
"private": true, "private": true,
"type": "module", "type": "module",
"imports": { "repository": {
"#src/*": "./build/*" "type": "git",
"url": "https://github.com/Thream/api"
}, },
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=16.0.0",
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"repository": {
"type": "git",
"url": "https://github.com/Thream/api"
},
"scripts": { "scripts": {
"build": "rimraf ./build && swc ./src --out-dir ./build", "build": "rimraf ./build && swc ./src --out-dir ./build && tsc",
"build:typescript": "tsc", "build:dev": "swc ./src --out-dir ./build --watch",
"start": "node --enable-source-maps build/index.js", "start": "node build/index.js",
"dev:build": "swc ./src --out-dir ./build --watch", "dev": "concurrently -k -n \"TypeScript,Node\" -p \"[{name}]\" -c \"blue,green\" \"npm run build:dev\" \"cross-env NODE_ENV=development nodemon build/index.js\"",
"dev": "concurrently --kill-others --names \"TypeScript,Node,Maildev,Prisma Studio\" \"npm run dev:build\" \"cross-env NODE_ENV=development node --watch --enable-source-maps build/index.js\" \"npm run maildev\" \"npm run prisma:studio\"",
"maildev": "maildev",
"generate": "plop", "generate": "plop",
"generate:jwt-secret": "node --enable-source-maps ./build/scripts/generate-jwt-secret.js",
"lint:commit": "commitlint", "lint:commit": "commitlint",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2", "lint:markdown": "markdownlint \"**/*.md\" --dot --ignore-path \".gitignore\"",
"lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore", "lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"",
"lint:prettier": "prettier . --check", "lint:prettier": "prettier \".\" --check",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test": "cross-env NODE_ENV=test node --enable-source-maps --test build/", "test": "cross-env NODE_ENV=test c8 tap",
"prisma:validate": "prisma validate",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:studio": "prisma studio --browser=none", "prisma:studio": "prisma studio",
"prisma:migrate:dev": "prisma migrate dev", "prisma:migrate:dev": "prisma migrate dev",
"prisma:migrate:deploy": "prisma migrate deploy", "prisma:migrate:deploy": "prisma migrate deploy",
"release": "semantic-release", "release": "semantic-release",
"postinstall": "husky install" "postinstall": "husky install"
}, },
"dependencies": { "dependencies": {
"@fastify/cors": "8.4.0", "@fastify/cors": "8.0.0",
"@fastify/helmet": "11.1.1", "@fastify/helmet": "9.1.0",
"@fastify/multipart": "8.0.0", "@fastify/multipart": "7.1.0",
"@fastify/rate-limit": "8.0.3", "@fastify/rate-limit": "7.0.0",
"@fastify/sensible": "5.5.0", "@fastify/sensible": "5.1.0",
"@fastify/swagger": "8.12.0", "@fastify/swagger": "7.4.0",
"@fastify/swagger-ui": "1.10.1", "@prisma/client": "3.12.0",
"@prisma/client": "5.4.2", "@sinclair/typebox": "0.23.5",
"@sinclair/typebox": "0.31.18", "@thream/socketio-jwt": "3.0.0",
"@thream/socketio-jwt": "3.1.3", "axios": "0.26.1",
"axios": "1.5.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"dotenv": "16.3.1", "dotenv": "16.0.1",
"ejs": "3.1.9", "ejs": "3.1.8",
"fastify": "4.24.3", "fastify": "4.2.0",
"fastify-plugin": "4.5.1", "fastify-plugin": "3.0.1",
"form-data": "4.0.0", "form-data": "4.0.0",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"jsonwebtoken": "9.0.2", "jsonwebtoken": "8.5.1",
"ms": "2.1.3", "ms": "2.1.3",
"nodemailer": "6.9.7", "nodemailer": "6.7.5",
"read-pkg": "8.1.0", "read-pkg": "7.1.0",
"socket.io": "4.7.2" "socket.io": "4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "18.4.3", "@commitlint/cli": "17.0.3",
"@commitlint/config-conventional": "18.4.3", "@commitlint/config-conventional": "17.0.3",
"@saithodev/semantic-release-backmerge": "4.0.1", "@saithodev/semantic-release-backmerge": "2.1.2",
"@semantic-release/git": "10.0.1", "@semantic-release/git": "10.0.1",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.57",
"@swc/core": "1.3.94", "@swc/core": "1.2.207",
"@tsconfig/strictest": "2.0.2", "@types/bcryptjs": "2.4.2",
"@types/bcryptjs": "2.4.5", "@types/busboy": "1.5.0",
"@types/busboy": "1.5.2", "@types/ejs": "3.1.1",
"@types/ejs": "3.1.4", "@types/http-errors": "1.8.2",
"@types/http-errors": "2.0.3", "@types/jsonwebtoken": "8.5.8",
"@types/jsonwebtoken": "9.0.4", "@types/ms": "0.7.31",
"@types/ms": "0.7.33", "@types/node": "18.0.0",
"@types/node": "20.8.7", "@types/nodemailer": "6.4.4",
"@types/nodemailer": "6.4.13", "@types/sinon": "10.0.12",
"@types/sinon": "10.0.20", "@types/tap": "15.0.7",
"@typescript-eslint/eslint-plugin": "6.9.0", "@typescript-eslint/eslint-plugin": "5.30.0",
"@typescript-eslint/parser": "6.9.0", "c8": "7.11.3",
"chokidar": "3.5.3", "concurrently": "7.2.2",
"concurrently": "8.2.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"editorconfig-checker": "5.1.1", "editorconfig-checker": "4.0.2",
"eslint": "8.52.0", "eslint": "8.18.0",
"eslint-config-conventions": "12.0.0", "eslint-config-conventions": "2.0.0",
"eslint-config-prettier": "9.0.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.29.0", "eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "5.0.1", "eslint-plugin-prettier": "4.1.0",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.0.0",
"eslint-plugin-unicorn": "48.0.1", "eslint-plugin-unicorn": "42.0.0",
"husky": "8.0.3", "husky": "8.0.1",
"lint-staged": "15.0.2", "lint-staged": "13.0.3",
"maildev": "2.1.0", "markdownlint-cli": "0.31.1",
"markdownlint-cli2": "0.10.0", "nodemon": "2.0.18",
"markdownlint-rule-relative-links": "2.1.0", "plop": "3.1.1",
"plop": "4.0.0", "prettier": "2.7.1",
"prettier": "3.0.3", "prisma": "3.12.0",
"prisma": "5.4.2", "rimraf": "3.0.2",
"rimraf": "5.0.5", "semantic-release": "19.0.3",
"semantic-release": "22.0.12", "sinon": "14.0.0",
"sinon": "17.0.0", "tap": "16.3.0",
"typescript": "5.2.2" "typescript": "4.7.4"
} }
} }

View File

@ -1,8 +1,8 @@
import { serviceGenerator } from "./generators/service/index.js" import { serviceGenerator } from './generators/service/index.js'
export default ( export default (
/** @type {import('plop').NodePlopAPI} */ /** @type {import('plop').NodePlopAPI} */
plop, plop
) => { ) => {
plop.setGenerator("service", serviceGenerator) plop.setGenerator('service', serviceGenerator)
} }

View File

@ -1,53 +1,51 @@
import type { User } from "@prisma/client" import { User } from '@prisma/client'
import sinon from "sinon" import sinon from 'sinon'
import { refreshTokenExample } from "#src/models/RefreshToken.js" import { refreshTokenExample } from '../../models/RefreshToken.js'
import type { UserJWT } from "#src/models/User.js" import { userExample, UserJWT } from '../../models/User.js'
import { userExample } from "#src/models/User.js" import { userSettingsExample } from '../../models/UserSettings.js'
import { userSettingsExample } from "#src/models/UserSettings.js"
import { import {
generateAccessToken, generateAccessToken,
generateRefreshToken, generateRefreshToken
} from "#src/tools/utils/jwtToken.js" } from '../../tools/utils/jwtToken.js'
import prisma from "#src/tools/database/prisma.js" import prisma from '../../tools/database/prisma.js'
const userStubValue = {
findUnique: async () => {
return userExample
},
}
const userSettingStubValue = {
findFirst: async () => {
return userSettingsExample
},
}
const oAuthStubValue = {
findMany: async () => {
return []
},
}
const refreshTokenStubValue = {
create: async () => {
return refreshTokenExample
},
}
export const authenticateUserTest = async (): Promise<{ export const authenticateUserTest = async (): Promise<{
accessToken: string accessToken: string
refreshToken: string refreshToken: string
user: User user: User
userStubValue: typeof userStubValue userStubValue: any
userSettingStubValue: typeof userSettingStubValue userSettingStubValue: any
oAuthStubValue: typeof oAuthStubValue oAuthStubValue: any
refreshTokenStubValue: typeof refreshTokenStubValue refreshTokenStubValue: any
}> => { }> => {
sinon.stub(prisma, "user").value(userStubValue) const userStubValue = {
sinon.stub(prisma, "userSetting").value(userSettingStubValue) findUnique: async () => {
sinon.stub(prisma, "oAuth").value(oAuthStubValue) return userExample
sinon.stub(prisma, "refreshToken").value(refreshTokenStubValue) }
}
const userSettingStubValue = {
findFirst: async () => {
return userSettingsExample
}
}
const oAuthStubValue = {
findMany: async () => {
return []
}
}
const refreshTokenStubValue = {
create: async () => {
return refreshTokenExample
}
}
sinon.stub(prisma, 'user').value(userStubValue)
sinon.stub(prisma, 'userSetting').value(userSettingStubValue)
sinon.stub(prisma, 'oAuth').value(oAuthStubValue)
sinon.stub(prisma, 'refreshToken').value(refreshTokenStubValue)
const userJWT: UserJWT = { const userJWT: UserJWT = {
currentStrategy: "Local", currentStrategy: 'Local',
id: 1, id: 1
} }
const accessToken = generateAccessToken(userJWT) const accessToken = generateAccessToken(userJWT)
const refreshToken = await generateRefreshToken(userJWT) const refreshToken = await generateRefreshToken(userJWT)
@ -58,6 +56,6 @@ export const authenticateUserTest = async (): Promise<{
userStubValue, userStubValue,
userSettingStubValue, userSettingStubValue,
oAuthStubValue, oAuthStubValue,
refreshTokenStubValue, refreshTokenStubValue
} }
} }

View File

@ -1,75 +1,43 @@
import dotenv from "dotenv" import dotenv from 'dotenv'
import fastify from "fastify" import fastify from 'fastify'
import fastifyCors from "@fastify/cors" import fastifyCors from '@fastify/cors'
import fastifySwagger from "@fastify/swagger" import fastifySwagger from '@fastify/swagger'
import fastifySwaggerUI from "@fastify/swagger-ui" import fastifyHelmet from '@fastify/helmet'
import fastifyHelmet from "@fastify/helmet" import fastifyRateLimit from '@fastify/rate-limit'
import fastifyRateLimit from "@fastify/rate-limit" import fastifySensible from '@fastify/sensible'
import fastifySensible from "@fastify/sensible"
import { readPackage } from "read-pkg"
import { services } from "#src/services/index.js" import { services } from './services/index.js'
import fastifySocketIo from "#src/tools/plugins/socket-io.js" import { swaggerOptions } from './tools/configurations/swaggerOptions.js'
import fastifySocketIo from './tools/plugins/socket-io.js'
dotenv.config() dotenv.config()
const packageJSON = await readPackage()
export const application = fastify({ export const application = fastify({
logger: process.env["NODE_ENV"] === "development", logger: process.env.NODE_ENV === 'development',
ajv: { ajv: {
customOptions: { customOptions: {
strict: "log", strict: 'log',
keywords: ["kind", "modifier"], keywords: ['kind', 'modifier'],
formats: { formats: {
full: true, full: true
}, }
}, }
}, }
}) })
await application.register(fastifyCors) await application.register(fastifyCors)
await application.register(fastifySensible) await application.register(fastifySensible)
await application.register(fastifySocketIo, { await application.register(fastifySocketIo, {
cors: { cors: {
origin: "*", origin: '*',
methods: "GET,HEAD,PUT,PATCH,POST,DELETE", methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
preflightContinue: false, preflightContinue: false,
optionsSuccessStatus: 204, optionsSuccessStatus: 204
}, }
}) })
await application.register(fastifyHelmet) await application.register(fastifyHelmet)
await application.register(fastifyRateLimit, { await application.register(fastifyRateLimit, {
max: 200, max: 200,
timeWindow: "1 minute", timeWindow: '1 minute'
})
await application.register(fastifySwagger, {
openapi: {
info: {
title: packageJSON.name,
description: packageJSON.description,
version: packageJSON.version,
},
tags: [
{ name: "users" },
{ name: "oauth2" },
{ name: "guilds" },
{ name: "channels" },
{ name: "messages" },
{ name: "members" },
],
components: {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
},
},
hideUntagged: true,
})
await application.register(fastifySwaggerUI, {
routePrefix: "/documentation",
staticCSP: true,
}) })
await application.register(fastifySwagger, swaggerOptions)
await application.register(services) await application.register(services)

View File

@ -1,8 +1,8 @@
import { application } from "#src/application.js" import { application } from './application.js'
import { HOST, PORT } from "#src/tools/configurations.js" import { HOST, PORT } from './tools/configurations/index.js'
const address = await application.listen({ const address = await application.listen({
port: PORT, port: PORT,
host: HOST, host: HOST
}) })
console.log(`Server listening at ${address}`) console.log('\u001B[36m%s\u001B[0m', `🚀 Server listening at ${address}`)

View File

@ -1,23 +1,23 @@
import { Type } from "@sinclair/typebox" import { Type } from '@sinclair/typebox'
import type { Channel } from "@prisma/client" import { Channel } from '@prisma/client'
import { date, id } from "#src/models/utils.js" import { date, id } from './utils.js'
import { guildExample } from "#src/models/Guild.js" import { guildExample } from './Guild.js'
export const types = [Type.Literal("text")] export const types = [Type.Literal('text')]
export const channelSchema = { export const channelSchema = {
id, id,
name: Type.String({ minLength: 1, maxLength: 20 }), name: Type.String({ minLength: 1, maxLength: 20 }),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
guildId: id, guildId: id
} }
export const channelExample: Channel = { export const channelExample: Channel = {
id: 1, id: 1,
name: "general", name: 'general',
guildId: guildExample.id, guildId: guildExample.id,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date()
} }

View File

@ -1,28 +1,28 @@
import type { Guild } from "@prisma/client" import { Guild } from '@prisma/client'
import { Type } from "@sinclair/typebox" import { Type } from '@sinclair/typebox'
import { date, id } from "#src/models/utils.js" import { date, id } from './utils.js'
export const guildSchema = { export const guildSchema = {
id, id,
name: Type.String({ minLength: 1, maxLength: 30 }), name: Type.String({ minLength: 1, maxLength: 30 }),
icon: Type.Union([ icon: Type.Union([
Type.String({ format: "uri-reference", minLength: 1 }), Type.String({ format: 'uri-reference', minLength: 1 }),
Type.Null(), Type.Null()
]), ]),
description: Type.Union([ description: Type.Union([
Type.String({ minLength: 1, maxLength: 160 }), Type.String({ minLength: 1, maxLength: 160 }),
Type.Null(), Type.Null()
]), ]),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt
} }
export const guildExample: Guild = { export const guildExample: Guild = {
id: 1, id: 1,
name: "GuildExample", name: 'GuildExample',
description: "guild example.", description: 'guild example.',
icon: null, icon: null,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date()
} }

View File

@ -1,9 +1,9 @@
import { Type } from "@sinclair/typebox" import { Type } from '@sinclair/typebox'
import type { Member } from "@prisma/client" import { Member } from '@prisma/client'
import { date, id } from "#src/models/utils.js" import { date, id } from './utils.js'
import { guildExample } from "#src/models/Guild.js" import { guildExample } from './Guild.js'
import { userExample } from "#src/models/User.js" import { userExample } from './User.js'
export const memberSchema = { export const memberSchema = {
id, id,
@ -11,7 +11,7 @@ export const memberSchema = {
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
userId: id, userId: id,
guildId: id, guildId: id
} }
export const memberExample: Member = { export const memberExample: Member = {
@ -20,5 +20,5 @@ export const memberExample: Member = {
userId: userExample.id, userId: userExample.id,
guildId: guildExample.id, guildId: guildExample.id,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date()
} }

View File

@ -1,34 +1,35 @@
import type { Message } from "@prisma/client" import { Message } from '@prisma/client'
import { Type } from "@sinclair/typebox" import { Type } from '@sinclair/typebox'
import { date, id } from "#src/models/utils.js" import { date, id } from './utils.js'
export const types = [Type.Literal("text"), Type.Literal("file")] export const types = [Type.Literal('text'), Type.Literal('file')]
export const messageSchema = { export const messageSchema = {
id, id,
value: Type.String({ value: Type.String({
minLength: 1, minLength: 1,
maxLength: 20_000, maxLength: 20_000
}), }),
type: Type.Union(types, { default: "text" }), type: Type.Union(types, { default: 'text' }),
mimetype: Type.String({ mimetype: Type.String({
maxLength: 127, maxLength: 127,
default: "text/plain", default: 'text/plain',
format: 'mimetype'
}), }),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
memberId: id, memberId: id,
channelId: id, channelId: id
} }
export const messageExample: Message = { export const messageExample: Message = {
id: 1, id: 1,
value: "Hello, world!", value: 'Hello, world!',
type: "text", type: 'text',
mimetype: "text/plain", mimetype: 'text/plain',
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
memberId: 1, memberId: 1,
channelId: 1, channelId: 1
} }

View File

@ -1,19 +1,19 @@
import { Type } from "@sinclair/typebox" import { Type } from '@sinclair/typebox'
import { date, id } from "#src/models/utils.js" import { date, id } from './utils.js'
export const providers = ["Google", "GitHub", "Discord"] as const export const providers = ['Google', 'GitHub', 'Discord'] as const
export const strategies = [...providers, "Local"] as const export const strategies = [...providers, 'Local'] as const
export const strategiesTypebox = strategies.map((strategy) => { export const strategiesTypebox = strategies.map((strategy) =>
return Type.Literal(strategy) Type.Literal(strategy)
}) )
export const providersTypebox = providers.map((provider) => { export const providersTypebox = providers.map((provider) =>
return Type.Literal(provider) Type.Literal(provider)
}) )
export type ProviderOAuth = (typeof providers)[number] export type ProviderOAuth = typeof providers[number]
export type AuthenticationStrategy = (typeof strategies)[number] export type AuthenticationStrategy = typeof strategies[number]
export const oauthSchema = { export const oauthSchema = {
id, id,
@ -21,5 +21,5 @@ export const oauthSchema = {
provider: Type.Union([...providersTypebox]), provider: Type.Union([...providersTypebox]),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
userId: id, userId: id
} }

View File

@ -1,21 +1,21 @@
import type { RefreshToken } from "@prisma/client" import { RefreshToken } from '@prisma/client'
import { Type } from "@sinclair/typebox" import { Type } from '@sinclair/typebox'
import { userExample } from "#src/models/User.js" import { userExample } from './User.js'
import { date, id } from "#src/models/utils.js" import { date, id } from './utils.js'
export const refreshTokensSchema = { export const refreshTokensSchema = {
id, id,
token: Type.String({ format: "uuid" }), token: Type.String(),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
userId: id, userId: id
} }
export const refreshTokenExample: RefreshToken = { export const refreshTokenExample: RefreshToken = {
id: 1, id: 1,
userId: userExample.id, userId: userExample.id,
token: "sometokenUUID", token: 'sometoken',
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date()
} }

View File

@ -1,21 +1,15 @@
import type { User } from "@prisma/client" import { User } from '@prisma/client'
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox"
import type { AuthenticationStrategy } from "#src/models/OAuth.js" import { AuthenticationStrategy, strategiesTypebox } from './OAuth.js'
import { strategiesTypebox } from "#src/models/OAuth.js" import { userSettingsSchema } from './UserSettings.js'
import { userSettingsSchema } from "#src/models/UserSettings.js" import { date, id } from './utils.js'
import { date, id } from "#src/models/utils.js"
export interface UserJWT { export interface UserJWT {
id: number id: number
currentStrategy: AuthenticationStrategy currentStrategy: AuthenticationStrategy
} }
export interface UserRefreshJWT extends UserJWT {
tokenUUID: string
}
export interface UserRequest { export interface UserRequest {
current: User current: User
currentStrategy: AuthenticationStrategy currentStrategy: AuthenticationStrategy
@ -25,21 +19,21 @@ export interface UserRequest {
export const userSchema = { export const userSchema = {
id, id,
name: Type.String({ minLength: 1, maxLength: 30 }), name: Type.String({ minLength: 1, maxLength: 30 }),
email: Type.String({ minLength: 1, maxLength: 254, format: "email" }), email: Type.String({ minLength: 1, maxLength: 254, format: 'email' }),
password: Type.String({ minLength: 1 }), password: Type.String({ minLength: 1 }),
logo: Type.String({ minLength: 1, format: "uri-reference" }), logo: Type.String({ minLength: 1, format: 'uri-reference' }),
status: Type.String({ minLength: 1, maxLength: 50 }), status: Type.String({ minLength: 1, maxLength: 50 }),
biography: Type.String({ minLength: 1, maxLength: 160 }), biography: Type.String({ minLength: 1, maxLength: 160 }),
website: Type.String({ website: Type.String({
minLength: 1, minLength: 1,
maxLength: 255, maxLength: 255,
format: "uri", format: 'uri'
}), }),
isConfirmed: Type.Boolean({ default: false }), isConfirmed: Type.Boolean({ default: false }),
temporaryToken: Type.String(), temporaryToken: Type.String(),
temporaryExpirationToken: Type.String({ format: "date-time" }), temporaryExpirationToken: Type.String({ format: 'date-time' }),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt
} }
export const userPublicWithoutSettingsSchema = { export const userPublicWithoutSettingsSchema = {
@ -52,20 +46,20 @@ export const userPublicWithoutSettingsSchema = {
website: Type.Union([userSchema.website, Type.Null()]), website: Type.Union([userSchema.website, Type.Null()]),
isConfirmed: userSchema.isConfirmed, isConfirmed: userSchema.isConfirmed,
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt
} }
export const userPublicSchema = { export const userPublicSchema = {
...userPublicWithoutSettingsSchema, ...userPublicWithoutSettingsSchema,
settings: Type.Object(userSettingsSchema), settings: Type.Object(userSettingsSchema)
} }
export const userCurrentSchema = Type.Object({ export const userCurrentSchema = Type.Object({
user: Type.Object({ user: Type.Object({
...userPublicSchema, ...userPublicSchema,
currentStrategy: Type.Union([...strategiesTypebox]), currentStrategy: Type.Union([...strategiesTypebox]),
strategies: Type.Array(Type.Union([...strategiesTypebox])), strategies: Type.Array(Type.Union([...strategiesTypebox]))
}), })
}) })
export const bodyUserSchema = Type.Object({ export const bodyUserSchema = Type.Object({
@ -73,23 +67,23 @@ export const bodyUserSchema = Type.Object({
name: userSchema.name, name: userSchema.name,
password: userSchema.password, password: userSchema.password,
theme: userSettingsSchema.theme, theme: userSettingsSchema.theme,
language: userSettingsSchema.language, language: userSettingsSchema.language
}) })
export type BodyUserSchemaType = Static<typeof bodyUserSchema> export type BodyUserSchemaType = Static<typeof bodyUserSchema>
export const userExample: User = { export const userExample: User = {
id: 1, id: 1,
name: "Divlo", name: 'Divlo',
email: "contact@divlo.fr", email: 'contact@divlo.fr',
password: "somepassword", password: 'somepassword',
logo: null, logo: null,
status: null, status: null,
biography: null, biography: null,
website: null, website: null,
isConfirmed: true, isConfirmed: true,
temporaryToken: "temporaryUUIDtoken", temporaryToken: 'temporaryUUIDtoken',
temporaryExpirationToken: new Date(), temporaryExpirationToken: new Date(),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date()
} }

View File

@ -1,11 +1,10 @@
import type { UserSetting } from "@prisma/client" import { UserSetting } from '@prisma/client'
import type { Static } from "@sinclair/typebox" import { Type, Static } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox"
import { date, id } from "#src/models/utils.js" import { date, id } from './utils.js'
export const languages = [Type.Literal("fr"), Type.Literal("en")] export const languages = [Type.Literal('fr'), Type.Literal('en')]
export const themes = [Type.Literal("light"), Type.Literal("dark")] export const themes = [Type.Literal('light'), Type.Literal('dark')]
export const userSettingsSchema = { export const userSettingsSchema = {
id, id,
@ -15,7 +14,7 @@ export const userSettingsSchema = {
isPublicGuilds: Type.Boolean(), isPublicGuilds: Type.Boolean(),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
userId: id, userId: id
} }
export type Theme = Static<typeof userSettingsSchema.theme> export type Theme = Static<typeof userSettingsSchema.theme>
@ -23,11 +22,11 @@ export type Language = Static<typeof userSettingsSchema.language>
export const userSettingsExample: UserSetting = { export const userSettingsExample: UserSetting = {
id: 1, id: 1,
theme: "dark", theme: 'dark',
language: "en", language: 'en',
isPublicEmail: false, isPublicEmail: false,
isPublicGuilds: false, isPublicGuilds: false,
userId: 1, userId: 1,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date()
} }

View File

@ -1,51 +1,51 @@
import { Type } from "@sinclair/typebox" import { Type } from '@sinclair/typebox'
export const date = { export const date = {
createdAt: Type.String({ createdAt: Type.String({
format: "date-time", format: 'date-time',
description: "Created date time", description: 'Created date time'
}), }),
updatedAt: Type.String({ updatedAt: Type.String({
format: "date-time", format: 'date-time',
description: "Last updated date time", description: 'Last updated date time'
}), })
} }
export const id = Type.Integer({ minimum: 1, description: "Unique identifier" }) export const id = Type.Integer({ minimum: 1, description: 'Unique identifier' })
export const redirectURI = Type.String({ format: "uri-reference" }) export const redirectURI = Type.String({ format: 'uri-reference' })
export const fastifyErrorsSchema = { export const fastifyErrorsSchema = {
400: { 400: {
statusCode: Type.Literal(400), statusCode: Type.Literal(400),
error: Type.Literal("Bad Request"), error: Type.Literal('Bad Request'),
message: Type.String(), message: Type.String()
}, },
401: { 401: {
statusCode: Type.Literal(401), statusCode: Type.Literal(401),
error: Type.Literal("Unauthorized"), error: Type.Literal('Unauthorized'),
message: Type.Literal("Unauthorized"), message: Type.Literal('Unauthorized')
}, },
403: { 403: {
statusCode: Type.Literal(403), statusCode: Type.Literal(403),
error: Type.Literal("Forbidden"), error: Type.Literal('Forbidden'),
message: Type.Literal("Forbidden"), message: Type.Literal('Forbidden')
}, },
404: { 404: {
statusCode: Type.Literal(404), statusCode: Type.Literal(404),
error: Type.Literal("Not Found"), error: Type.Literal('Not Found'),
message: Type.String(), message: Type.Literal('Not Found')
}, },
431: { 431: {
statusCode: Type.Literal(431), statusCode: Type.Literal(431),
error: Type.Literal("Request Header Fields Too Large"), error: Type.Literal('Request Header Fields Too Large'),
message: Type.String(), message: Type.String()
}, },
500: { 500: {
statusCode: Type.Literal(500), statusCode: Type.Literal(500),
error: Type.Literal("Internal Server Error"), error: Type.Literal('Internal Server Error'),
message: Type.Literal("Something went wrong"), message: Type.Literal('Something went wrong')
}, }
} }
export const fastifyErrors = { export const fastifyErrors = {
@ -54,5 +54,5 @@ export const fastifyErrors = {
403: Type.Object(fastifyErrorsSchema[403]), 403: Type.Object(fastifyErrorsSchema[403]),
404: Type.Object(fastifyErrorsSchema[404]), 404: Type.Object(fastifyErrorsSchema[404]),
431: Type.Object(fastifyErrorsSchema[431]), 431: Type.Object(fastifyErrorsSchema[431]),
500: Type.Object(fastifyErrorsSchema[500]), 500: Type.Object(fastifyErrorsSchema[500])
} }

View File

@ -1,3 +0,0 @@
import crypto from "node:crypto"
console.log(crypto.randomBytes(256).toString("base64"))

View File

@ -1,30 +1,28 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { channelExample } from '../../../../models/Channel.js'
import { memberExample } from '../../../../models/Member.js'
import { application } from "#src/application.js" await tap.test('DELETE /channels/[channelId]', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
await test("DELETE /channels/[channelId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const defaultChannelId = 5 const defaultChannelId = 5
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, },
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId, id: defaultChannelId
} }
}, },
count: async () => { count: async () => {
@ -32,114 +30,114 @@ await test("DELETE /channels/[channelId]", async (t) => {
}, },
delete: async () => { delete: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.id, channelExample.id) t.equal(responseJson.id, channelExample.id)
assert.strictEqual(responseJson.name, channelExample.name) t.equal(responseJson.name, channelExample.name)
assert.strictEqual(responseJson.guildId, channelExample.guildId) t.equal(responseJson.guildId, channelExample.guildId)
assert.strictEqual(responseJson.defaultChannelId, defaultChannelId) t.equal(responseJson.defaultChannelId, defaultChannelId)
}) })
await t.test("fails if there is only one channel", async () => { await t.test('fails if there is only one channel', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, },
count: async () => { count: async () => {
return 1 return 1
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}) })
await t.test("fails if the channel is not found", async () => { await t.test('fails if the channel is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is not found", async () => { await t.test('fails if the member is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is not owner", async () => { await t.test('fails if the member is not owner', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false, isOwner: false
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}) })
}) })

View File

@ -1,98 +1,96 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { channelExample } from '../../../../models/Channel.js'
import { memberExample } from '../../../../models/Member.js'
import { application } from "#src/application.js" await tap.test('GET /channels/[channelId]', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
await test("GET /channels/[channelId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.channel.id, channelExample.id) t.equal(responseJson.channel.id, channelExample.id)
assert.strictEqual(responseJson.channel.name, channelExample.name) t.equal(responseJson.channel.name, channelExample.name)
assert.strictEqual(responseJson.channel.guildId, channelExample.guildId) t.equal(responseJson.channel.guildId, channelExample.guildId)
}) })
await t.test("fails with not found member", async () => { await t.test('fails with not found member', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Channel not found") t.equal(responseJson.message, 'Channel not found')
}) })
await t.test("fails with not found channel", async () => { await t.test('fails with not found channel', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return null return null
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Channel not found") t.equal(responseJson.message, 'Channel not found')
}) })
await t.test("fails with unauthenticated user", async () => { await t.test('fails with unauthenticated user', async (t) => {
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/channels/1", url: '/channels/1'
}) })
assert.strictEqual(response.statusCode, 401) t.equal(response.statusCode, 401)
}) })
}) })

View File

@ -1,131 +1,129 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { channelExample } from '../../../../models/Channel.js'
import { memberExample } from '../../../../models/Member.js'
import { application } from "#src/application.js" const newName = 'new channel name'
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
const newName = "new channel name" await tap.test('PUT /channels/[channelId]', async (t) => {
await test("PUT /channels/[channelId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const defaultChannelId = 5 const defaultChannelId = 5
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, },
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId, id: defaultChannelId
} }
}, },
update: async () => { update: async () => {
return { return {
...channelExample, ...channelExample,
name: newName, name: newName
}
} }
},
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { name: newName }, payload: { name: newName }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.id, channelExample.id) t.equal(responseJson.id, channelExample.id)
assert.strictEqual(responseJson.name, newName) t.equal(responseJson.name, newName)
assert.strictEqual(responseJson.guildId, channelExample.guildId) t.equal(responseJson.guildId, channelExample.guildId)
assert.strictEqual(responseJson.defaultChannelId, defaultChannelId) t.equal(responseJson.defaultChannelId, defaultChannelId)
}) })
await t.test("fails if the channel is not found", async () => { await t.test('fails if the channel is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return null return null
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { name: newName }, payload: { name: newName }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is not found", async () => { await t.test('fails if the member is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { name: newName }, payload: { name: newName }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is not owner", async () => { await t.test('fails if the member is not owner', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false, isOwner: false
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { name: newName }, payload: { name: newName }
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}) })
}) })

View File

@ -1,38 +1,37 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../models/Channel.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id, channelId: channelSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const deleteServiceSchema: FastifySchema = { const deleteServiceSchema: FastifySchema = {
description: "DELETE a channel with its id.", description: 'DELETE a channel with its id.',
tags: ["channels"] as string[], tags: ['channels'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
...channelSchema, ...channelSchema,
defaultChannelId: channelSchema.id, defaultChannelId: channelSchema.id
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const deleteChannelService: FastifyPluginAsync = async (fastify) => { export const deleteChannelService: FastifyPluginAsync = async (fastify) => {
@ -41,61 +40,61 @@ export const deleteChannelService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "DELETE", method: 'DELETE',
url: "/channels/:channelId", url: '/channels/:channelId',
schema: deleteServiceSchema, schema: deleteServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user, params } = request const { user } = request
const { channelId } = params const { channelId } = request.params
const channelCheck = await prisma.channel.findUnique({ const channelCheck = await prisma.channel.findUnique({
where: { id: channelId }, where: { id: channelId }
}) })
if (channelCheck == null) { if (channelCheck == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId: channelCheck.guildId, userId: user.current.id }, where: { guildId: channelCheck.guildId, userId: user.current.id }
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest("You should be a member owner") throw fastify.httpErrors.badRequest('You should be a member owner')
} }
const channelCount = await prisma.channel.count({ const channelCount = await prisma.channel.count({
where: { guildId: channelCheck.guildId }, where: { guildId: channelCheck.guildId }
}) })
if (channelCount <= 1) { if (channelCount <= 1) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
"The guild should have at least one channel", 'The guild should have at least one channel'
) )
} }
const channel = await prisma.channel.delete({ const channel = await prisma.channel.delete({
where: { id: channelId }, where: { id: channelId }
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: member.guildId }, where: { guildId: member.guildId }
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
} }
const item = { const item = {
...channel, ...channel,
defaultChannelId: defaultChannel.id, defaultChannelId: defaultChannel.id
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "channels", event: 'channels',
guildId: member.guildId, guildId: member.guildId,
payload: { payload: {
action: "delete", action: 'delete',
item, item
}, }
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
}, }
}) })
} }

View File

@ -1,37 +1,36 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../models/Channel.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id, channelId: channelSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: "GET a channel with its id.", description: 'GET a channel with its id.',
tags: ["channels"] as string[], tags: ['channels'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
channel: Type.Object(channelSchema), channel: Type.Object(channelSchema)
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const getChannelByIdService: FastifyPluginAsync = async (fastify) => { export const getChannelByIdService: FastifyPluginAsync = async (fastify) => {
@ -40,8 +39,8 @@ export const getChannelByIdService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "GET", method: 'GET',
url: "/channels/:channelId", url: '/channels/:channelId',
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -49,19 +48,19 @@ export const getChannelByIdService: FastifyPluginAsync = async (fastify) => {
} }
const { channelId } = request.params const { channelId } = request.params
const channel = await prisma.channel.findUnique({ const channel = await prisma.channel.findUnique({
where: { id: channelId }, where: { id: channelId }
}) })
if (channel == null) { if (channel == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id }, where: { guildId: channel.guildId, userId: request.user.current.id }
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
reply.statusCode = 200 reply.statusCode = 200
return { channel } return { channel }
}, }
}) })
} }

View File

@ -1,117 +1,115 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../../application.js'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js'
import { channelExample } from '../../../../../models/Channel.js'
import { memberExample } from '../../../../../models/Member.js'
import { userExample } from '../../../../../models/User.js'
import { messageExample } from '../../../../../models/Message.js'
import { application } from "#src/application.js" await tap.test('GET /channels/[channelId]/messages', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
import { userExample } from "#src/models/User.js"
import { messageExample } from "#src/models/Message.js"
await test("GET /channels/[channelId]/messages", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample, user: userExample
}
} }
},
}) })
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findMany: async () => { findMany: async () => {
return [messageExample] return [messageExample]
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.length, 1) t.equal(responseJson.length, 1)
assert.strictEqual(responseJson[0].id, messageExample.id) t.equal(responseJson[0].id, messageExample.id)
assert.strictEqual(responseJson[0].value, messageExample.value) t.equal(responseJson[0].value, messageExample.value)
assert.strictEqual(responseJson[0].type, messageExample.type) t.equal(responseJson[0].type, messageExample.type)
assert.strictEqual(responseJson[0].mimetype, messageExample.mimetype) t.equal(responseJson[0].mimetype, messageExample.mimetype)
assert.strictEqual(responseJson[0].member.id, memberExample.id) t.equal(responseJson[0].member.id, memberExample.id)
assert.strictEqual(responseJson[0].member.isOwner, memberExample.isOwner) t.equal(responseJson[0].member.isOwner, memberExample.isOwner)
assert.strictEqual(responseJson[0].member.user.id, userExample.id) t.equal(responseJson[0].member.user.id, userExample.id)
assert.strictEqual(responseJson[0].member.user.name, userExample.name) t.equal(responseJson[0].member.user.name, userExample.name)
}) })
await t.test("fails with not found channel", async () => { await t.test('fails with not found channel', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return null return null
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample, user: userExample
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Channel not found") t.equal(responseJson.message, 'Channel not found')
}) })
await t.test("fails with not found member", async () => { await t.test('fails with not found member', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Channel not found") t.equal(responseJson.message, 'Channel not found')
}) })
await t.test("fails with unauthenticated user", async () => { await t.test('fails with unauthenticated user', async (t) => {
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/channels/1/messages`, url: `/channels/1/messages`
}) })
assert.strictEqual(response.statusCode, 401) t.equal(response.statusCode, 401)
}) })
}) })

View File

@ -1,137 +1,135 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../../application.js'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js'
import { channelExample } from '../../../../../models/Channel.js'
import { memberExample } from '../../../../../models/Member.js'
import { userExample } from '../../../../../models/User.js'
import { messageExample } from '../../../../../models/Message.js'
import { application } from "#src/application.js" await tap.test('POST /channels/[channelId]/messages', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
import { userExample } from "#src/models/User.js"
import { messageExample } from "#src/models/Message.js"
await test("POST /channels/[channelId]/messages", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample, user: userExample
}
} }
},
}) })
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
create: async () => { create: async () => {
return messageExample return messageExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { value: messageExample.value }, payload: { value: messageExample.value }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 201) t.equal(response.statusCode, 201)
assert.strictEqual(responseJson.id, messageExample.id) t.equal(responseJson.id, messageExample.id)
assert.strictEqual(responseJson.value, messageExample.value) t.equal(responseJson.value, messageExample.value)
assert.strictEqual(responseJson.type, messageExample.type) t.equal(responseJson.type, messageExample.type)
assert.strictEqual(responseJson.mimetype, messageExample.mimetype) t.equal(responseJson.mimetype, messageExample.mimetype)
assert.strictEqual(responseJson.member.id, memberExample.id) t.equal(responseJson.member.id, memberExample.id)
assert.strictEqual(responseJson.member.isOwner, memberExample.isOwner) t.equal(responseJson.member.isOwner, memberExample.isOwner)
assert.strictEqual(responseJson.member.user.id, userExample.id) t.equal(responseJson.member.user.id, userExample.id)
assert.strictEqual(responseJson.member.user.name, userExample.name) t.equal(responseJson.member.user.name, userExample.name)
}) })
await t.test("fails with no message value", async () => { await t.test('fails with no message value', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample, user: userExample
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: {}, payload: {}
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}) })
await t.test("fails with not found channel", async () => { await t.test('fails with not found channel', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return null return null
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample, user: userExample
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: "/channels/5/messages", url: '/channels/5/messages',
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { value: messageExample.value }, payload: { value: messageExample.value }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Channel not found") t.equal(responseJson.message, 'Channel not found')
}) })
await t.test("fails with not found member", async () => { await t.test('fails with not found member', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { value: messageExample.value }, payload: { value: messageExample.value }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Channel not found") t.equal(responseJson.message, 'Channel not found')
}) })
}) })

View File

@ -1,34 +1,33 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { messageSchema } from "#src/models/Message.js" import { messageSchema } from '../../../../models/Message.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" import { userPublicWithoutSettingsSchema } from '../../../../models/User.js'
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationObjectSchema, queryPaginationObjectSchema
} from "#src/tools/database/pagination.js" } from '../../../../tools/database/pagination.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../../models/Channel.js'
type QuerySchemaType = Static<typeof queryPaginationObjectSchema> type QuerySchemaType = Static<typeof queryPaginationObjectSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id, channelId: channelSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: "GET all the messages of a channel by its id.", description: 'GET all the messages of a channel by its id.',
tags: ["messages"] as string[], tags: ['messages'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
querystring: queryPaginationObjectSchema, querystring: queryPaginationObjectSchema,
@ -38,20 +37,20 @@ const getServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), })
}), })
), ),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const getMessagesByChannelIdService: FastifyPluginAsync = async ( export const getMessagesByChannelIdService: FastifyPluginAsync = async (
fastify, fastify
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -59,8 +58,8 @@ export const getMessagesByChannelIdService: FastifyPluginAsync = async (
Params: Parameters Params: Parameters
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: "GET", method: 'GET',
url: "/channels/:channelId/messages", url: '/channels/:channelId/messages',
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -68,21 +67,21 @@ export const getMessagesByChannelIdService: FastifyPluginAsync = async (
} }
const { channelId } = request.params const { channelId } = request.params
const channel = await prisma.channel.findUnique({ const channel = await prisma.channel.findUnique({
where: { id: channelId }, where: { id: channelId }
}) })
if (channel == null) { if (channel == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id }, where: { guildId: channel.guildId, userId: request.user.current.id }
}) })
if (memberCheck == null) { if (memberCheck == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const messagesRequest = await prisma.message.findMany({ const messagesRequest = await prisma.message.findMany({
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
orderBy: { createdAt: "desc" }, orderBy: { createdAt: 'desc' },
where: { channelId }, where: { channelId }
}) })
const messages = await Promise.all( const messages = await Promise.all(
messagesRequest.reverse().map(async (message) => { messagesRequest.reverse().map(async (message) => {
@ -98,10 +97,10 @@ export const getMessagesByChannelIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
}, }
}, }
}) })
return { return {
...message, ...message,
@ -109,14 +108,14 @@ export const getMessagesByChannelIdService: FastifyPluginAsync = async (
...member, ...member,
user: { user: {
...member?.user, ...member?.user,
email: null, email: null
},
},
} }
}), }
}
})
) )
reply.statusCode = 200 reply.statusCode = 200
return messages return messages
}, }
}) })
} }

View File

@ -1,35 +1,34 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { messageSchema } from "#src/models/Message.js" import { messageSchema } from '../../../../models/Message.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../../models/Channel.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" import { userPublicWithoutSettingsSchema } from '../../../../models/User.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id, channelId: channelSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const bodyPostServiceSchema = Type.Object({ const bodyPostServiceSchema = Type.Object({
value: messageSchema.value, value: messageSchema.value
}) })
type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema> type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: description:
"POST a new message (text) in a specific channel using its channelId.", 'POST a new message (text) in a specific channel using its channelId.',
tags: ["messages"] as string[], tags: ['messages'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPostServiceSchema, body: bodyPostServiceSchema,
params: parametersSchema, params: parametersSchema,
@ -38,19 +37,19 @@ const postServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), })
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const postMessageByChannelIdService: FastifyPluginAsync = async ( export const postMessageByChannelIdService: FastifyPluginAsync = async (
fastify, fastify
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -58,8 +57,8 @@ export const postMessageByChannelIdService: FastifyPluginAsync = async (
Body: BodyPostServiceSchemaType Body: BodyPostServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: "POST", method: 'POST',
url: "/channels/:channelId/messages", url: '/channels/:channelId/messages',
schema: postServiceSchema, schema: postServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -67,10 +66,10 @@ export const postMessageByChannelIdService: FastifyPluginAsync = async (
} }
const { channelId } = request.params const { channelId } = request.params
const channel = await prisma.channel.findUnique({ const channel = await prisma.channel.findUnique({
where: { id: channelId }, where: { id: channelId }
}) })
if (channel == null) { if (channel == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id }, where: { guildId: channel.guildId, userId: request.user.current.id },
@ -84,23 +83,23 @@ export const postMessageByChannelIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
}, }
}, }
}) })
if (memberCheck == null) { if (memberCheck == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const { value } = request.body const { value } = request.body
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
value, value,
type: "text", type: 'text',
mimetype: "text/plain", mimetype: 'text/plain',
channelId, channelId,
memberId: memberCheck.id, memberId: memberCheck.id
}, }
}) })
const item = { const item = {
...message, ...message,
@ -108,17 +107,17 @@ export const postMessageByChannelIdService: FastifyPluginAsync = async (
...memberCheck, ...memberCheck,
user: { user: {
...memberCheck.user, ...memberCheck.user,
email: null, email: null
}, }
}, }
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "messages", event: 'messages',
guildId: item.member.guildId, guildId: item.member.guildId,
payload: { action: "create", item }, payload: { action: 'create', item }
}) })
reply.statusCode = 201 reply.statusCode = 201
return item return item
}, }
}) })
} }

View File

@ -1,33 +1,32 @@
import type { Static } from "@sinclair/typebox" import { Type, Static } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify" import fastifyMultipart from '@fastify/multipart'
import fastifyMultipart from "@fastify/multipart"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../../../tools/plugins/authenticateUser.js'
import { messageSchema } from "#src/models/Message.js" import { messageSchema } from '../../../../../models/Message.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" import { userPublicWithoutSettingsSchema } from '../../../../../models/User.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../../../models/Channel.js'
import { uploadFile } from "#src/tools/utils/uploadFile.js" import { uploadFile } from '../../../../../tools/utils/uploadFile.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id, channelId: channelSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: description:
"POST a new message (file) in a specific channel using its channelId.", 'POST a new message (file) in a specific channel using its channelId.',
tags: ["messages"] as string[], tags: ['messages'] as string[],
consumes: ["multipart/form-data"] as string[], consumes: ['multipart/form-data'] as string[],
produces: ["application/json"] as string[], produces: ['application/json'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -35,20 +34,20 @@ const postServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), })
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
431: fastifyErrors[431], 431: fastifyErrors[431],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async ( export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
fastify, fastify
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -57,8 +56,8 @@ export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "POST", method: 'POST',
url: "/channels/:channelId/messages/uploads", url: '/channels/:channelId/messages/uploads',
schema: postServiceSchema, schema: postServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -66,10 +65,10 @@ export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
} }
const { channelId } = request.params const { channelId } = request.params
const channel = await prisma.channel.findUnique({ const channel = await prisma.channel.findUnique({
where: { id: channelId }, where: { id: channelId }
}) })
if (channel == null) { if (channel == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id }, where: { guildId: channel.guildId, userId: request.user.current.id },
@ -83,27 +82,27 @@ export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
}, }
}, }
}) })
if (memberCheck == null) { if (memberCheck == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const file = await uploadFile({ const file = await uploadFile({
fastify, fastify,
request, request,
folderInUploadsFolder: "messages", folderInUploadsFolder: 'messages'
}) })
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
value: file.pathToStoreInDatabase, value: file.pathToStoreInDatabase,
type: "file", type: 'file',
mimetype: file.mimetype, mimetype: file.mimetype,
channelId, channelId,
memberId: memberCheck.id, memberId: memberCheck.id
}, }
}) })
reply.statusCode = 201 reply.statusCode = 201
const item = { const item = {
@ -112,16 +111,16 @@ export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
...memberCheck, ...memberCheck,
user: { user: {
...memberCheck.user, ...memberCheck.user,
email: null, email: null
}, }
}, }
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "messages", event: 'messages',
guildId: item.member.guildId, guildId: item.member.guildId,
payload: { action: "create", item }, payload: { action: 'create', item }
}) })
return item return item
}, }
}) })
} }

View File

@ -1,45 +1,44 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../models/Channel.js'
const bodyPutServiceSchema = Type.Object({ const bodyPutServiceSchema = Type.Object({
name: channelSchema.name, name: channelSchema.name
}) })
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema> type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id, channelId: channelSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: "UPDATE a channel with its id.", description: 'UPDATE a channel with its id.',
tags: ["channels"] as string[], tags: ['channels'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
body: bodyPutServiceSchema, body: bodyPutServiceSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
...channelSchema, ...channelSchema,
defaultChannelId: channelSchema.id, defaultChannelId: channelSchema.id
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const putChannelService: FastifyPluginAsync = async (fastify) => { export const putChannelService: FastifyPluginAsync = async (fastify) => {
@ -49,55 +48,55 @@ export const putChannelService: FastifyPluginAsync = async (fastify) => {
Body: BodyPutServiceSchemaType Body: BodyPutServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: "PUT", method: 'PUT',
url: "/channels/:channelId", url: '/channels/:channelId',
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user, params, body } = request const { user } = request
const { channelId } = params const { channelId } = request.params
const { name } = body const { name } = request.body
const channelCheck = await prisma.channel.findUnique({ const channelCheck = await prisma.channel.findUnique({
where: { id: channelId }, where: { id: channelId }
}) })
if (channelCheck == null) { if (channelCheck == null) {
throw fastify.httpErrors.notFound("Channel not found") throw fastify.httpErrors.notFound('Channel not found')
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId: channelCheck.guildId, userId: user.current.id }, where: { guildId: channelCheck.guildId, userId: user.current.id }
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest("You should be a member owner") throw fastify.httpErrors.badRequest('You should be a member owner')
} }
const channel = await prisma.channel.update({ const channel = await prisma.channel.update({
where: { id: channelId }, where: { id: channelId },
data: { name }, data: { name }
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: member.guildId }, where: { guildId: member.guildId }
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
} }
const item = { const item = {
...channel, ...channel,
defaultChannelId: defaultChannel.id, defaultChannelId: defaultChannel.id
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "channels", event: 'channels',
guildId: member.guildId, guildId: member.guildId,
payload: { payload: {
action: "update", action: 'update',
item, item
}, }
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
}, }
}) })
} }

View File

@ -1,11 +1,11 @@
import type { FastifyPluginAsync } from "fastify" import { FastifyPluginAsync } from 'fastify'
import { deleteChannelService } from "./[channelId]/delete.js" import { deleteChannelService } from './[channelId]/delete.js'
import { getChannelByIdService } from "./[channelId]/get.js" import { getChannelByIdService } from './[channelId]/get.js'
import { getMessagesByChannelIdService } from "./[channelId]/messages/get.js" import { getMessagesByChannelIdService } from './[channelId]/messages/get.js'
import { postMessageByChannelIdService } from "./[channelId]/messages/post.js" import { postMessageByChannelIdService } from './[channelId]/messages/post.js'
import { postMessageUploadsByChannelIdService } from "./[channelId]/messages/uploads/post.js" import { postMessageUploadsByChannelIdService } from './[channelId]/messages/uploads/post.js'
import { putChannelService } from "./[channelId]/put.js" import { putChannelService } from './[channelId]/put.js'
export const channelsService: FastifyPluginAsync = async (fastify) => { export const channelsService: FastifyPluginAsync = async (fastify) => {
await fastify.register(getChannelByIdService) await fastify.register(getChannelByIdService)

View File

@ -1,89 +1,84 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { memberExample } from '../../../../models/Member.js'
import { guildExample } from '../../../../models/Guild.js'
import { application } from "#src/application.js" await tap.test('DELETE /guilds/[guildId]', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
await test("DELETE /guilds/[guildId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds and delete the guild", async () => { await t.test('succeeds and delete the guild', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: true, isOwner: true,
guild: guildExample, guild: guildExample
}
} }
},
}) })
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
delete: async () => { delete: async () => {
return guildExample return guildExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.id, guildExample.id) t.equal(responseJson.id, guildExample.id)
assert.strictEqual(responseJson.name, guildExample.name) t.equal(responseJson.name, guildExample.name)
assert.strictEqual(responseJson.description, guildExample.description) t.equal(responseJson.description, guildExample.description)
}) })
await t.test("fails if the guild doesn't exist", async () => { await t.test("fails if the guild doesn't exist", async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the user is not the owner", async () => { await t.test('fails if the user is not the owner', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false, isOwner: false,
guild: guildExample, guild: guildExample
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
assert.strictEqual( t.equal(responseJson.message, 'You should be an owner of the guild')
responseJson.message,
"You should be an owner of the guild",
)
}) })
}) })

View File

@ -1,84 +1,82 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import { application } from "#src/application.js" import prisma from '../../../../tools/database/prisma.js'
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js" import { memberExample } from '../../../../models/Member.js'
import prisma from "#src/tools/database/prisma.js" import { guildExample } from '../../../../models/Guild.js'
import { memberExample } from "#src/models/Member.js" import { userExample } from '../../../../models/User.js'
import { guildExample } from "#src/models/Guild.js" import { channelExample } from '../../../../models/Channel.js'
import { userExample } from "#src/models/User.js"
import { channelExample } from "#src/models/Channel.js"
const defaultChannelId = 5 const defaultChannelId = 5
await test("GET /guilds/[guildId]", async (t) => { await tap.test('GET /guilds/[guildId]', async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken, user } = await authenticateUserTest() const { accessToken, user } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
guild: guildExample, guild: guildExample,
user: userExample, user: userExample
}
} }
},
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId, id: defaultChannelId
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.member.id, memberExample.id) t.equal(responseJson.member.id, memberExample.id)
assert.strictEqual(responseJson.member.isOwner, memberExample.isOwner) t.equal(responseJson.member.isOwner, memberExample.isOwner)
assert.strictEqual(responseJson.member.user.name, user.name) t.equal(responseJson.member.user.name, user.name)
assert.strictEqual(responseJson.member.user.email, null) t.equal(responseJson.member.user.email, null)
assert.strictEqual(responseJson.guild.id, guildExample.id) t.equal(responseJson.guild.id, guildExample.id)
assert.strictEqual(responseJson.guild.name, guildExample.name) t.equal(responseJson.guild.name, guildExample.name)
assert.strictEqual(responseJson.guild.defaultChannelId, defaultChannelId) t.equal(responseJson.guild.defaultChannelId, defaultChannelId)
}) })
await t.test("fails with not found member/guild", async () => { await t.test('fails with not found member/guild', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/guilds/1", url: '/guilds/1',
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Member not found") t.equal(responseJson.message, 'Member not found')
}) })
await t.test("fails with unauthenticated user", async () => { await t.test('fails with unauthenticated user', async (t) => {
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/guilds/1", url: '/guilds/1'
}) })
assert.strictEqual(response.statusCode, 401) t.equal(response.statusCode, 401)
}) })
}) })

View File

@ -1,118 +1,113 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import { application } from "#src/application.js" import prisma from '../../../../tools/database/prisma.js'
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js" import { memberExample } from '../../../../models/Member.js'
import prisma from "#src/tools/database/prisma.js" import { guildExample } from '../../../../models/Guild.js'
import { memberExample } from "#src/models/Member.js" import { channelExample } from '../../../../models/Channel.js'
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
const defaultChannelId = 5 const defaultChannelId = 5
const newName = "New guild name" const newName = 'New guild name'
const newDescription = "New guild description" const newDescription = 'New guild description'
await test("PUT /guilds/[guildId]", async (t) => { await tap.test('PUT /guilds/[guildId]', async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds and edit the guild", async () => { await t.test('succeeds and edit the guild', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: true, isOwner: true,
guild: guildExample, guild: guildExample
}
} }
},
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId, id: defaultChannelId
}
} }
},
}) })
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
update: async () => { update: async () => {
return { return {
...guildExample, ...guildExample,
name: newName, name: newName,
description: newDescription, description: newDescription
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { payload: {
name: newName, name: newName,
description: newDescription, description: newDescription
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.name, newName) t.equal(responseJson.name, newName)
assert.strictEqual(responseJson.description, newDescription) t.equal(responseJson.description, newDescription)
assert.strictEqual(responseJson.defaultChannelId, defaultChannelId) t.equal(responseJson.defaultChannelId, defaultChannelId)
}) })
await t.test("fails if the guild doesn't exist", async () => { await t.test("fails if the guild doesn't exist", async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { payload: {
name: newName, name: newName,
description: newDescription, description: newDescription
}, }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the user is not the owner", async () => { await t.test('fails if the user is not the owner', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false, isOwner: false,
guild: guildExample, guild: guildExample
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { payload: {
name: newName, name: newName,
description: newDescription, description: newDescription
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
assert.strictEqual( t.equal(responseJson.message, 'You should be an owner of the guild')
responseJson.message,
"You should be an owner of the guild",
)
}) })
}) })

View File

@ -1,71 +1,69 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../../application.js'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js'
import { memberExample } from '../../../../../models/Member.js'
import { guildExample } from '../../../../../models/Guild.js'
import { channelExample } from '../../../../../models/Channel.js'
import { application } from "#src/application.js" await tap.test('GET /guilds/[guildId]/channels', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
await test("GET /guilds/[guildId]/channels", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findMany: async () => { findMany: async () => {
return [channelExample] return [channelExample]
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/guilds/${guildExample.id}/channels`, url: `/guilds/${guildExample.id}/channels`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.length, 1) t.equal(responseJson.length, 1)
assert.strictEqual(responseJson[0].id, channelExample.id) t.equal(responseJson[0].id, channelExample.id)
assert.strictEqual(responseJson[0].name, channelExample.name) t.equal(responseJson[0].name, channelExample.name)
assert.strictEqual(responseJson[0].guildId, channelExample.guildId) t.equal(responseJson[0].guildId, channelExample.guildId)
}) })
await t.test("fails with not found member/guild", async () => { await t.test('fails with not found member/guild', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/guilds/1/channels", url: '/guilds/1/channels',
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Member not found") t.equal(responseJson.message, 'Member not found')
}) })
await t.test("fails with unauthenticated user", async () => { await t.test('fails with unauthenticated user', async (t) => {
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/guilds/1/channels", url: '/guilds/1/channels'
}) })
assert.strictEqual(response.statusCode, 401) t.equal(response.statusCode, 401)
}) })
}) })

View File

@ -1,92 +1,90 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../../application.js'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import { application } from "#src/application.js" import prisma from '../../../../../tools/database/prisma.js'
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js" import { memberExample } from '../../../../../models/Member.js'
import prisma from "#src/tools/database/prisma.js" import { guildExample } from '../../../../../models/Guild.js'
import { memberExample } from "#src/models/Member.js" import { channelExample } from '../../../../../models/Channel.js'
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
const defaultChannelId = 5 const defaultChannelId = 5
await test("POST /guilds/[guildId]/channels", async (t) => { await tap.test('POST /guilds/[guildId]/channels', async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId, id: defaultChannelId
} }
}, },
create: async () => { create: async () => {
return channelExample return channelExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/guilds/${guildExample.id}/channels`, url: `/guilds/${guildExample.id}/channels`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { name: channelExample.name }, payload: { name: channelExample.name }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 201) t.equal(response.statusCode, 201)
assert.strictEqual(responseJson.id, channelExample.id) t.equal(responseJson.id, channelExample.id)
assert.strictEqual(responseJson.name, channelExample.name) t.equal(responseJson.name, channelExample.name)
assert.strictEqual(responseJson.guildId, channelExample.guildId) t.equal(responseJson.guildId, channelExample.guildId)
assert.strictEqual(responseJson.defaultChannelId, defaultChannelId) t.equal(responseJson.defaultChannelId, defaultChannelId)
}) })
await t.test("fails if the member is not found", async () => { await t.test('fails if the member is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/guilds/${guildExample.id}/channels`, url: `/guilds/${guildExample.id}/channels`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { name: channelExample.name }, payload: { name: channelExample.name }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is not owner", async () => { await t.test('fails if the member is not owner', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false, isOwner: false
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/guilds/${guildExample.id}/channels`, url: `/guilds/${guildExample.id}/channels`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { name: channelExample.name }, payload: { name: channelExample.name }
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}) })
}) })

View File

@ -1,32 +1,31 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../../models/Guild.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../../models/Channel.js'
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationObjectSchema, queryPaginationObjectSchema
} from "#src/tools/database/pagination.js" } from '../../../../tools/database/pagination.js'
type QuerySchemaType = Static<typeof queryPaginationObjectSchema> type QuerySchemaType = Static<typeof queryPaginationObjectSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: "GET all the channels of a guild with its id.", description: 'GET all the channels of a guild with its id.',
tags: ["channels"] as string[], tags: ['channels'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
querystring: queryPaginationObjectSchema, querystring: queryPaginationObjectSchema,
@ -36,12 +35,12 @@ const getServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const getChannelsByGuildIdService: FastifyPluginAsync = async ( export const getChannelsByGuildIdService: FastifyPluginAsync = async (
fastify, fastify
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -49,8 +48,8 @@ export const getChannelsByGuildIdService: FastifyPluginAsync = async (
Params: Parameters Params: Parameters
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: "GET", method: 'GET',
url: "/guilds/:guildId/channels", url: '/guilds/:guildId/channels',
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -58,19 +57,19 @@ export const getChannelsByGuildIdService: FastifyPluginAsync = async (
} }
const { guildId } = request.params const { guildId } = request.params
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: request.user.current.id }, where: { guildId, userId: request.user.current.id }
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
const channels = await prisma.channel.findMany({ const channels = await prisma.channel.findMany({
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
where: { where: {
guildId, guildId
}, }
}) })
reply.statusCode = 200 reply.statusCode = 200
return channels return channels
}, }
}) })
} }

View File

@ -1,46 +1,45 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../../models/Channel.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../../models/Guild.js'
const bodyPostServiceSchema = Type.Object({ const bodyPostServiceSchema = Type.Object({
name: channelSchema.name, name: channelSchema.name
}) })
type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema> type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const postChannelServiceSchema: FastifySchema = { const postChannelServiceSchema: FastifySchema = {
description: "Create a channel.", description: 'Create a channel.',
tags: ["channels"] as string[], tags: ['channels'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPostServiceSchema, body: bodyPostServiceSchema,
params: parametersSchema, params: parametersSchema,
response: { response: {
201: Type.Object({ 201: Type.Object({
...channelSchema, ...channelSchema,
defaultChannelId: channelSchema.id, defaultChannelId: channelSchema.id
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const postChannelService: FastifyPluginAsync = async (fastify) => { export const postChannelService: FastifyPluginAsync = async (fastify) => {
@ -50,51 +49,51 @@ export const postChannelService: FastifyPluginAsync = async (fastify) => {
Body: BodyPostServiceSchemaType Body: BodyPostServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: "POST", method: 'POST',
url: "/guilds/:guildId/channels", url: '/guilds/:guildId/channels',
schema: postChannelServiceSchema, schema: postChannelServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user, params, body } = request const { user } = request
const { guildId } = params const { guildId } = request.params
const { name } = body const { name } = request.body
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: user.current.id }, where: { guildId, userId: user.current.id }
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest("You should be a member owner") throw fastify.httpErrors.badRequest('You should be a member owner')
} }
const channel = await prisma.channel.create({ const channel = await prisma.channel.create({
data: { data: {
name, name,
guildId, guildId
}, }
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: member.guildId }, where: { guildId: member.guildId }
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
} }
const item = { const item = {
...channel, ...channel,
defaultChannelId: defaultChannel.id, defaultChannelId: defaultChannel.id
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "channels", event: 'channels',
guildId, guildId,
payload: { payload: {
action: "create", action: 'create',
item, item
}, }
}) })
reply.statusCode = 201 reply.statusCode = 201
return item return item
}, }
}) })
} }

View File

@ -1,25 +1,24 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../models/Guild.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const deleteServiceSchema: FastifySchema = { const deleteServiceSchema: FastifySchema = {
description: "DELETE a guild with the guildId.", description: 'DELETE a guild with the guildId.',
tags: ["guilds"] as string[], tags: ['guilds'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -28,8 +27,8 @@ const deleteServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const deleteGuildByIdService: FastifyPluginAsync = async (fastify) => { export const deleteGuildByIdService: FastifyPluginAsync = async (fastify) => {
@ -38,8 +37,8 @@ export const deleteGuildByIdService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "DELETE", method: 'DELETE',
url: "/guilds/:guildId", url: '/guilds/:guildId',
schema: deleteServiceSchema, schema: deleteServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -49,30 +48,30 @@ export const deleteGuildByIdService: FastifyPluginAsync = async (fastify) => {
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: request.user.current.id }, where: { guildId, userId: request.user.current.id },
include: { include: {
guild: true, guild: true
}, }
}) })
if (member == null || member.guild == null) { if (member == null || member.guild == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
"You should be an owner of the guild", 'You should be an owner of the guild'
) )
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "guilds", event: 'guilds',
guildId: member.guildId, guildId: member.guildId,
payload: { payload: {
action: "delete", action: 'delete',
item: member.guild, item: member.guild
}, }
}) })
const guild = await prisma.guild.delete({ const guild = await prisma.guild.delete({
where: { id: member.guildId }, where: { id: member.guildId }
}) })
reply.statusCode = 200 reply.statusCode = 200
return guild return guild
}, }
}) })
} }

View File

@ -1,59 +1,58 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../models/Guild.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" import { userPublicWithoutSettingsSchema } from '../../../models/User.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../models/Channel.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: "GET a guild member with the guildId.", description: 'GET a guild member with the guildId.',
tags: ["guilds"] as string[], tags: ['guilds'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
guild: Type.Object({ guild: Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: channelSchema.id, defaultChannelId: channelSchema.id
}), }),
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), })
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const getGuildMemberByIdService: FastifyPluginAsync = async ( export const getGuildMemberByIdService: FastifyPluginAsync = async (
fastify, fastify
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "GET", method: 'GET',
url: "/guilds/:guildId", url: '/guilds/:guildId',
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -72,17 +71,17 @@ export const getGuildMemberByIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
},
guild: true,
}, },
guild: true
}
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: member.guildId }, where: { guildId: member.guildId }
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
@ -90,18 +89,18 @@ export const getGuildMemberByIdService: FastifyPluginAsync = async (
const item = { const item = {
guild: { guild: {
...member.guild, ...member.guild,
defaultChannelId: defaultChannel.id, defaultChannelId: defaultChannel.id
}, },
member: { member: {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null, email: null
}, }
}, }
} }
reply.statusCode = 200 reply.statusCode = 200
return item return item
}, }
}) })
} }

View File

@ -1,44 +1,43 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify" import fastifyMultipart from '@fastify/multipart'
import fastifyMultipart from "@fastify/multipart"
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../../models/utils.js'
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../tools/database/prisma.js'
import { uploadFile } from "#src/tools/utils/uploadFile.js" import { uploadFile } from '../../../../tools/utils/uploadFile.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../../models/Guild.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../../models/Channel.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: "Edit the icon of the guild with its id", description: 'Edit the icon of the guild with its id',
tags: ["guilds"] as string[], tags: ['guilds'] as string[],
consumes: ["multipart/form-data"] as string[], consumes: ['multipart/form-data'] as string[],
produces: ["application/json"] as string[], produces: ['application/json'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: channelSchema.id, defaultChannelId: channelSchema.id
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
431: fastifyErrors[431], 431: fastifyErrors[431],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const putGuildIconById: FastifyPluginAsync = async (fastify) => { export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
@ -49,8 +48,8 @@ export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "PUT", method: 'PUT',
url: "/guilds/:guildId/icon", url: '/guilds/:guildId/icon',
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -64,14 +63,14 @@ export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
const file = await uploadFile({ const file = await uploadFile({
fastify, fastify,
request, request,
folderInUploadsFolder: "guilds", folderInUploadsFolder: 'guilds'
}) })
await prisma.guild.update({ await prisma.guild.update({
where: { id: guildId }, where: { id: guildId },
data: { icon: file.pathToStoreInDatabase }, data: { icon: file.pathToStoreInDatabase }
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: guild.id }, where: { guildId: guild.id }
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
@ -79,15 +78,15 @@ export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
const item = { const item = {
...guild, ...guild,
icon: file.pathToStoreInDatabase, icon: file.pathToStoreInDatabase,
defaultChannelId: defaultChannel.id, defaultChannelId: defaultChannel.id
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "guilds", event: 'guilds',
guildId: guild.id, guildId: guild.id,
payload: { action: "update", item }, payload: { action: 'update', item }
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
}, }
}) })
} }

View File

@ -1,71 +1,69 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../../application.js'
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js'
import { memberExample } from '../../../../../models/Member.js'
import { guildExample } from '../../../../../models/Guild.js'
import { userExample } from '../../../../../models/User.js'
import { application } from "#src/application.js" await tap.test('GET /guilds/[guildId]/members', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { userExample } from "#src/models/User.js"
await test("GET /guilds/[guildId]/members", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, },
findMany: async () => { findMany: async () => {
return [{ ...memberExample, user: userExample }] return [{ ...memberExample, user: userExample }]
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/guilds/${guildExample.id}/members`, url: `/guilds/${guildExample.id}/members`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.length, 1) t.equal(responseJson.length, 1)
assert.strictEqual(responseJson[0].id, memberExample.id) t.equal(responseJson[0].id, memberExample.id)
assert.strictEqual(responseJson[0].isOwner, memberExample.isOwner) t.equal(responseJson[0].isOwner, memberExample.isOwner)
assert.strictEqual(responseJson[0].user.id, userExample.id) t.equal(responseJson[0].user.id, userExample.id)
assert.strictEqual(responseJson[0].user.name, userExample.name) t.equal(responseJson[0].user.name, userExample.name)
assert.strictEqual(responseJson[0].user.email, null) t.equal(responseJson[0].user.email, null)
}) })
await t.test("fails with not found member/guild", async () => { await t.test('fails with not found member/guild', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/guilds/1/members", url: '/guilds/1/members',
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "Member not found") t.equal(responseJson.message, 'Member not found')
}) })
await t.test("fails with unauthenticated user", async () => { await t.test('fails with unauthenticated user', async (t) => {
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/guilds/1/members", url: '/guilds/1/members'
}) })
assert.strictEqual(response.statusCode, 401) t.equal(response.statusCode, 401)
}) })
}) })

View File

@ -1,33 +1,32 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../../models/Guild.js'
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationObjectSchema, queryPaginationObjectSchema
} from "#src/tools/database/pagination.js" } from '../../../../tools/database/pagination.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" import { userPublicWithoutSettingsSchema } from '../../../../models/User.js'
type QuerySchemaType = Static<typeof queryPaginationObjectSchema> type QuerySchemaType = Static<typeof queryPaginationObjectSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: "GET all the members of a guild with its id.", description: 'GET all the members of a guild with its id.',
tags: ["members"] as string[], tags: ['members'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
querystring: queryPaginationObjectSchema, querystring: queryPaginationObjectSchema,
@ -35,19 +34,19 @@ const getServiceSchema: FastifySchema = {
200: Type.Array( 200: Type.Array(
Type.Object({ Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), })
), ),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const getMembersByGuildIdService: FastifyPluginAsync = async ( export const getMembersByGuildIdService: FastifyPluginAsync = async (
fastify, fastify
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -55,8 +54,8 @@ export const getMembersByGuildIdService: FastifyPluginAsync = async (
Params: Parameters Params: Parameters
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: "GET", method: 'GET',
url: "/guilds/:guildId/members", url: '/guilds/:guildId/members',
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -64,14 +63,14 @@ export const getMembersByGuildIdService: FastifyPluginAsync = async (
} }
const { guildId } = request.params const { guildId } = request.params
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { guildId, userId: request.user.current.id }, where: { guildId, userId: request.user.current.id }
}) })
if (memberCheck == null) { if (memberCheck == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
const membersRequest = await prisma.member.findMany({ const membersRequest = await prisma.member.findMany({
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
orderBy: { createdAt: "asc" }, orderBy: { createdAt: 'asc' },
where: { guildId }, where: { guildId },
include: { include: {
user: { user: {
@ -83,22 +82,22 @@ export const getMembersByGuildIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
}, }
}, }
}) })
const members = membersRequest.map((member) => { const members = membersRequest.map((member) => {
return { return {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null, email: null
}, }
} }
}) })
reply.statusCode = 200 reply.statusCode = 200
return members return members
}, }
}) })
} }

View File

@ -1,117 +1,115 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../../../application.js'
import { authenticateUserTest } from '../../../../../../__test__/utils/authenticateUserTest.js'
import { application } from "#src/application.js" import prisma from '../../../../../../tools/database/prisma.js'
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js" import { memberExample } from '../../../../../../models/Member.js'
import prisma from "#src/tools/database/prisma.js" import { guildExample } from '../../../../../../models/Guild.js'
import { memberExample } from "#src/models/Member.js" import { userExample } from '../../../../../../models/User.js'
import { guildExample } from "#src/models/Guild.js" import { channelExample } from '../../../../../../models/Channel.js'
import { userExample } from "#src/models/User.js"
import { channelExample } from "#src/models/Channel.js"
const defaultChannelId = 5 const defaultChannelId = 5
await test("POST /guilds/[guildId]/members/join", async (t) => { await tap.test('POST /guilds/[guildId]/members/join', async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, },
create: async () => { create: async () => {
return { ...memberExample, user: userExample } return { ...memberExample, user: userExample }
}, }
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findFirst: async () => { findFirst: async () => {
return channelExample return channelExample
}, }
}) })
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
findUnique: async () => { findUnique: async () => {
return guildExample return guildExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/guilds/${guildExample.id}/members/join`, url: `/guilds/${guildExample.id}/members/join`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 201) t.equal(response.statusCode, 201)
assert.strictEqual(responseJson.id, memberExample.id) t.equal(responseJson.id, memberExample.id)
assert.strictEqual(responseJson.userId, memberExample.userId) t.equal(responseJson.userId, memberExample.userId)
assert.strictEqual(responseJson.user.name, userExample.name) t.equal(responseJson.user.name, userExample.name)
assert.strictEqual(responseJson.user.email, null) t.equal(responseJson.user.email, null)
assert.strictEqual(responseJson.guild.id, guildExample.id) t.equal(responseJson.guild.id, guildExample.id)
assert.strictEqual(responseJson.guild.name, guildExample.name) t.equal(responseJson.guild.name, guildExample.name)
assert.strictEqual(responseJson.guild.defaultChannelId, channelExample.id) t.equal(responseJson.guild.defaultChannelId, channelExample.id)
}) })
await t.test("fails if the guild is not found", async () => { await t.test('fails if the guild is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
findUnique: async () => { findUnique: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/guilds/${guildExample.id}/members/join`, url: `/guilds/${guildExample.id}/members/join`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the user is already in the guild", async () => { await t.test('fails if the user is already in the guild', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, }
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId, id: defaultChannelId
}
} }
},
}) })
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
findUnique: async () => { findUnique: async () => {
return guildExample return guildExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: `/guilds/${guildExample.id}/members/join`, url: `/guilds/${guildExample.id}/members/join`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
assert.strictEqual(responseJson.defaultChannelId, defaultChannelId) t.equal(responseJson.defaultChannelId, defaultChannelId)
}) })
}) })

View File

@ -1,28 +1,31 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../../tools/database/prisma.js'
import { fastifyErrors, fastifyErrorsSchema, id } from "#src/models/utils.js" import {
import authenticateUser from "#src/tools/plugins/authenticateUser.js" fastifyErrors,
import { guildSchema } from "#src/models/Guild.js" fastifyErrorsSchema,
import { memberSchema } from "#src/models/Member.js" id
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" } from '../../../../../models/utils.js'
import { channelSchema } from "#src/models/Channel.js" import authenticateUser from '../../../../../tools/plugins/authenticateUser.js'
import { guildSchema } from '../../../../../models/Guild.js'
import { memberSchema } from '../../../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from '../../../../../models/User.js'
import { channelSchema } from '../../../../../models/Channel.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: "Join a guild (create a member).", description: 'Join a guild (create a member).',
tags: ["members"] as string[], tags: ['members'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -30,19 +33,19 @@ const postServiceSchema: FastifySchema = {
...memberSchema, ...memberSchema,
guild: Type.Object({ guild: Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: id, defaultChannelId: id
}), }),
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), }),
400: Type.Object({ 400: Type.Object({
...fastifyErrorsSchema[400], ...fastifyErrorsSchema[400],
defaultChannelId: channelSchema.id, defaultChannelId: channelSchema.id
}), }),
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const postMemberService: FastifyPluginAsync = async (fastify) => { export const postMemberService: FastifyPluginAsync = async (fastify) => {
@ -51,27 +54,27 @@ export const postMemberService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "POST", method: 'POST',
url: "/guilds/:guildId/members/join", url: '/guilds/:guildId/members/join',
schema: postServiceSchema, schema: postServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user, params } = request const { user } = request
const { guildId } = params const { guildId } = request.params
const guild = await prisma.guild.findUnique({ const guild = await prisma.guild.findUnique({
where: { where: {
id: guildId, id: guildId
}, }
}) })
if (guild == null) { if (guild == null) {
throw fastify.httpErrors.notFound("Guild not found") throw fastify.httpErrors.notFound('Guild not found')
} }
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { where: {
guildId, guildId
}, }
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
@ -79,22 +82,22 @@ export const postMemberService: FastifyPluginAsync = async (fastify) => {
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { where: {
userId: user.current.id, userId: user.current.id,
guildId: guild.id, guildId: guild.id
}, }
}) })
if (memberCheck != null) { if (memberCheck != null) {
throw fastify.httpErrors.createError( throw fastify.httpErrors.createError(
400, 400,
"You are already in the guild", 'You are already in the guild',
{ {
defaultChannelId: defaultChannel.id, defaultChannelId: defaultChannel.id
}, }
) )
} }
const member = await prisma.member.create({ const member = await prisma.member.create({
data: { data: {
guildId, guildId,
userId: user.current.id, userId: user.current.id
}, },
include: { include: {
user: { user: {
@ -106,33 +109,33 @@ export const postMemberService: FastifyPluginAsync = async (fastify) => {
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
}, }
}, }
}) })
const item = { const item = {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null, email: null
}, },
guild: { guild: {
...guild, ...guild,
defaultChannelId: defaultChannel.id, defaultChannelId: defaultChannel.id
}, }
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "members", event: 'members',
guildId, guildId,
payload: { payload: {
action: "create", action: 'create',
item, item
}, }
}) })
reply.statusCode = 201 reply.statusCode = 201
return item return item
}, }
}) })
} }

View File

@ -1,82 +1,80 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../../../application.js'
import { authenticateUserTest } from '../../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../../tools/database/prisma.js'
import { memberExample } from '../../../../../../models/Member.js'
import { guildExample } from '../../../../../../models/Guild.js'
import { application } from "#src/application.js" await tap.test('DELETE /guilds/[guildId]/members/leave', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
await test("DELETE /guilds/[guildId]/members/leave", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const member = { const member = {
...memberExample, ...memberExample,
isOwner: false, isOwner: false
} }
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return member return member
}, },
delete: async () => { delete: async () => {
return member return member
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/guilds/${guildExample.id}/members/leave`, url: `/guilds/${guildExample.id}/members/leave`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.id, member.id) t.equal(responseJson.id, member.id)
assert.strictEqual(responseJson.isOwner, member.isOwner) t.equal(responseJson.isOwner, member.isOwner)
assert.strictEqual(responseJson.userId, member.userId) t.equal(responseJson.userId, member.userId)
}) })
await t.test("fails if the member is not found", async () => { await t.test('fails if the member is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/guilds/${guildExample.id}/members/leave`, url: `/guilds/${guildExample.id}/members/leave`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is owner", async () => { await t.test('fails if the member is owner', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const member = { const member = {
...memberExample, ...memberExample,
isOwner: true, isOwner: true
} }
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return member return member
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/guilds/${guildExample.id}/members/leave`, url: `/guilds/${guildExample.id}/members/leave`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}) })
}) })

View File

@ -1,26 +1,25 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../../../models/Guild.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../../../../models/Member.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const deleteServiceSchema: FastifySchema = { const deleteServiceSchema: FastifySchema = {
description: "Leave a guild (delete a member).", description: 'Leave a guild (delete a member).',
tags: ["members"] as string[], tags: ['members'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -29,8 +28,8 @@ const deleteServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const deleteMemberService: FastifyPluginAsync = async (fastify) => { export const deleteMemberService: FastifyPluginAsync = async (fastify) => {
@ -39,37 +38,37 @@ export const deleteMemberService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "DELETE", method: 'DELETE',
url: "/guilds/:guildId/members/leave", url: '/guilds/:guildId/members/leave',
schema: deleteServiceSchema, schema: deleteServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user, params } = request const { user } = request
const { guildId } = params const { guildId } = request.params
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: user.current.id }, where: { guildId, userId: user.current.id }
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
if (member.isOwner) { if (member.isOwner) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
"The member owner can't leave the guild (you can delete it instead)", "The member owner can't leave the guild (you can delete it instead)"
) )
} }
await prisma.member.delete({ where: { id: member.id } }) await prisma.member.delete({ where: { id: member.id } })
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "members", event: 'members',
guildId, guildId,
payload: { payload: {
action: "delete", action: 'delete',
item: member, item: member
}, }
}) })
reply.statusCode = 200 reply.statusCode = 200
return member return member
}, }
}) })
} }

View File

@ -1,48 +1,47 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../models/Guild.js'
import { parseStringNullish } from "#src/tools/utils/parseStringNullish.js" import { parseStringNullish } from '../../../tools/utils/parseStringNullish.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../../models/Channel.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id, guildId: guildSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const bodyPutServiceSchema = Type.Object({ const bodyPutServiceSchema = Type.Object({
name: Type.Optional(guildSchema.name), name: Type.Optional(guildSchema.name),
description: Type.Optional(guildSchema.description), description: Type.Optional(guildSchema.description)
}) })
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema> type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: "Update a guild with the guildId.", description: 'Update a guild with the guildId.',
tags: ["guilds"] as string[], tags: ['guilds'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPutServiceSchema, body: bodyPutServiceSchema,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: channelSchema.id, defaultChannelId: channelSchema.id
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const putGuildByIdService: FastifyPluginAsync = async (fastify) => { export const putGuildByIdService: FastifyPluginAsync = async (fastify) => {
@ -52,8 +51,8 @@ export const putGuildByIdService: FastifyPluginAsync = async (fastify) => {
Body: BodyPutServiceSchemaType Body: BodyPutServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: "PUT", method: 'PUT',
url: "/guilds/:guildId", url: '/guilds/:guildId',
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -64,44 +63,41 @@ export const putGuildByIdService: FastifyPluginAsync = async (fastify) => {
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: request.user.current.id }, where: { guildId, userId: request.user.current.id },
include: { include: {
guild: true, guild: true
}, }
}) })
if (member == null || member.guild == null) { if (member == null || member.guild == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
"You should be an owner of the guild", 'You should be an owner of the guild'
) )
} }
const guild = await prisma.guild.update({ const guild = await prisma.guild.update({
where: { id: guildId }, where: { id: guildId },
data: { data: {
name: name ?? member.guild.name, name: name ?? member.guild.name,
description: parseStringNullish( description: parseStringNullish(member.guild.description, description)
member.guild.description, }
description,
),
},
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: guild.id }, where: { guildId: guild.id }
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
} }
const item = { const item = {
...guild, ...guild,
defaultChannelId: defaultChannel.id, defaultChannelId: defaultChannel.id
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "guilds", event: 'guilds',
guildId: guild.id, guildId: guild.id,
payload: { action: "update", item }, payload: { action: 'update', item }
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
}, }
}) })
} }

View File

@ -1,49 +1,47 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../application.js'
import { authenticateUserTest } from '../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../tools/database/prisma.js'
import { memberExample } from '../../../models/Member.js'
import { guildExample } from '../../../models/Guild.js'
import { channelExample } from '../../../models/Channel.js'
import { application } from "#src/application.js" await tap.test('GET /guilds', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
await test("GET /guilds", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
findUnique: async () => { findUnique: async () => {
return guildExample return guildExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findMany: async () => { findMany: async () => {
return [memberExample] return [memberExample]
}, }
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
findFirst: async () => { findFirst: async () => {
return channelExample return channelExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/guilds", url: '/guilds',
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.length, 1) t.equal(responseJson.length, 1)
assert.strictEqual(responseJson[0].name, guildExample.name) t.equal(responseJson[0].name, guildExample.name)
assert.strictEqual(responseJson[0].description, guildExample.description) t.equal(responseJson[0].description, guildExample.description)
assert.strictEqual(responseJson[0].defaultChannelId, channelExample.id) t.equal(responseJson[0].defaultChannelId, channelExample.id)
}) })
}) })

View File

@ -1,82 +1,77 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../application.js'
import { authenticateUserTest } from '../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../tools/database/prisma.js'
import { memberExample } from '../../../models/Member.js'
import { guildExample } from '../../../models/Guild.js'
import { channelExample } from '../../../models/Channel.js'
import { userExample } from '../../../models/User.js'
import { application } from "#src/application.js" await tap.test('POST /guilds', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
import { userExample } from "#src/models/User.js"
await test("POST /guilds", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken, user } = await authenticateUserTest() const { accessToken, user } = await authenticateUserTest()
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
create: async () => { create: async () => {
return guildExample return guildExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
create: async () => { create: async () => {
return memberExample return memberExample
}, },
findUnique: async () => { findUnique: async () => {
return { return {
...memberExample, ...memberExample,
...userExample, ...userExample
}
} }
},
}) })
sinon.stub(prisma, "channel").value({ sinon.stub(prisma, 'channel').value({
create: async () => { create: async () => {
return channelExample return channelExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: "/guilds", url: '/guilds',
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { payload: {
name: guildExample.name, name: guildExample.name,
description: guildExample.description, description: guildExample.description
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 201) t.equal(response.statusCode, 201)
assert.strictEqual(responseJson.guild.id, guildExample.id) t.equal(responseJson.guild.id, guildExample.id)
assert.strictEqual(responseJson.guild.name, guildExample.name) t.equal(responseJson.guild.name, guildExample.name)
assert.strictEqual(responseJson.guild.description, guildExample.description) t.equal(responseJson.guild.description, guildExample.description)
assert.strictEqual(responseJson.guild.members.length, 1) t.equal(responseJson.guild.members.length, 1)
assert.strictEqual(responseJson.guild.members[0].userId, user.id) t.equal(responseJson.guild.members[0].userId, user.id)
assert.strictEqual(responseJson.guild.members[0].user.name, user.name) t.equal(responseJson.guild.members[0].user.name, user.name)
assert.strictEqual(responseJson.guild.members[0].guildId, guildExample.id) t.equal(responseJson.guild.members[0].guildId, guildExample.id)
assert.strictEqual( t.equal(responseJson.guild.members[0].isOwner, memberExample.isOwner)
responseJson.guild.members[0].isOwner, t.equal(responseJson.guild.channels.length, 1)
memberExample.isOwner, t.equal(responseJson.guild.channels[0].id, channelExample.id)
) t.equal(responseJson.guild.channels[0].guildId, guildExample.id)
assert.strictEqual(responseJson.guild.channels.length, 1)
assert.strictEqual(responseJson.guild.channels[0].id, channelExample.id)
assert.strictEqual(responseJson.guild.channels[0].guildId, guildExample.id)
}) })
await t.test("fails with empty name and description", async () => { await t.test('fails with empty name and description', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const response = await application.inject({ const response = await application.inject({
method: "POST", method: 'POST',
url: "/guilds", url: '/guilds',
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}) })
}) })

View File

@ -1,39 +1,38 @@
import type { Static } from "@sinclair/typebox" import { Type, Static } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../tools/database/prisma.js'
import { fastifyErrors, id } from "#src/models/utils.js" import { fastifyErrors, id } from '../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../models/Guild.js'
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationObjectSchema, queryPaginationObjectSchema
} from "#src/tools/database/pagination.js" } from '../../tools/database/pagination.js'
type QuerySchemaType = Static<typeof queryPaginationObjectSchema> type QuerySchemaType = Static<typeof queryPaginationObjectSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: "GET all the guilds of an user.", description: 'GET all the guilds of an user.',
tags: ["guilds"] as string[], tags: ['guilds'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
querystring: queryPaginationObjectSchema, querystring: queryPaginationObjectSchema,
response: { response: {
200: Type.Array( 200: Type.Array(
Type.Object({ Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: id, defaultChannelId: id
}), })
), ),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const getGuilds: FastifyPluginAsync = async (fastify) => { export const getGuilds: FastifyPluginAsync = async (fastify) => {
@ -42,8 +41,8 @@ export const getGuilds: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: "GET", method: 'GET',
url: "/guilds", url: '/guilds',
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -52,29 +51,29 @@ export const getGuilds: FastifyPluginAsync = async (fastify) => {
const membersRequest = await prisma.member.findMany({ const membersRequest = await prisma.member.findMany({
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
where: { where: {
userId: request.user.current.id, userId: request.user.current.id
}, }
}) })
const guilds = await Promise.all( const guilds = await Promise.all(
membersRequest.map(async (member) => { membersRequest.map(async (member) => {
const channel = await prisma.channel.findFirst({ const channel = await prisma.channel.findFirst({
where: { where: {
guildId: member.guildId, guildId: member.guildId
}, }
}) })
const guild = await prisma.guild.findUnique({ const guild = await prisma.guild.findUnique({
where: { where: {
id: member.guildId, id: member.guildId
}, }
}) })
return { return {
...guild, ...guild,
defaultChannelId: channel?.id, defaultChannelId: channel?.id
} }
}), })
) )
reply.statusCode = 200 reply.statusCode = 200
return guilds return guilds
}, }
}) })
} }

View File

@ -1,17 +1,17 @@
import type { FastifyPluginAsync } from "fastify" import { FastifyPluginAsync } from 'fastify'
import { getGuilds } from "./get.js" import { getGuilds } from './get.js'
import { postGuilds } from "./post.js" import { postGuilds } from './post.js'
import { getGuildsPublic } from "./public/get.js" import { getGuildsPublic } from './public/get.js'
import { getChannelsByGuildIdService } from "./[guildId]/channels/get.js" import { getChannelsByGuildIdService } from './[guildId]/channels/get.js'
import { postChannelService } from "./[guildId]/channels/post.js" import { postChannelService } from './[guildId]/channels/post.js'
import { deleteGuildByIdService } from "./[guildId]/delete.js" import { deleteGuildByIdService } from './[guildId]/delete.js'
import { getGuildMemberByIdService } from "./[guildId]/get.js" import { getGuildMemberByIdService } from './[guildId]/get.js'
import { putGuildIconById } from "./[guildId]/icon/put.js" import { putGuildIconById } from './[guildId]/icon/put.js'
import { getMembersByGuildIdService } from "./[guildId]/members/get.js" import { getMembersByGuildIdService } from './[guildId]/members/get.js'
import { postMemberService } from "./[guildId]/members/join/post.js" import { postMemberService } from './[guildId]/members/join/post.js'
import { deleteMemberService } from "./[guildId]/members/leave/delete.js" import { deleteMemberService } from './[guildId]/members/leave/delete.js'
import { putGuildByIdService } from "./[guildId]/put.js" import { putGuildByIdService } from './[guildId]/put.js'
export const guildsService: FastifyPluginAsync = async (fastify) => { export const guildsService: FastifyPluginAsync = async (fastify) => {
await fastify.register(postGuilds) await fastify.register(postGuilds)

View File

@ -1,30 +1,29 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../models/Guild.js'
import { channelSchema } from "#src/models/Channel.js" import { channelSchema } from '../../models/Channel.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../models/Member.js'
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" import { userPublicWithoutSettingsSchema } from '../../models/User.js'
import { parseStringNullish } from "#src/tools/utils/parseStringNullish.js" import { parseStringNullish } from '../../tools/utils/parseStringNullish.js'
const bodyPostServiceSchema = Type.Object({ const bodyPostServiceSchema = Type.Object({
name: guildSchema.name, name: guildSchema.name,
description: guildSchema.description, description: guildSchema.description
}) })
type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema> type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: "Create a guild.", description: 'Create a guild.',
tags: ["guilds"] as string[], tags: ['guilds'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPostServiceSchema, body: bodyPostServiceSchema,
response: { response: {
@ -35,16 +34,16 @@ const postServiceSchema: FastifySchema = {
members: Type.Array( members: Type.Array(
Type.Object({ Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), })
), )
}), })
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const postGuilds: FastifyPluginAsync = async (fastify) => { export const postGuilds: FastifyPluginAsync = async (fastify) => {
@ -53,8 +52,8 @@ export const postGuilds: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Body: BodyPostServiceSchemaType Body: BodyPostServiceSchemaType
}>({ }>({
method: "POST", method: 'POST',
url: "/guilds", url: '/guilds',
schema: postServiceSchema, schema: postServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -62,37 +61,37 @@ export const postGuilds: FastifyPluginAsync = async (fastify) => {
} }
const { name, description } = request.body const { name, description } = request.body
const guild = await prisma.guild.create({ const guild = await prisma.guild.create({
data: { name, description: parseStringNullish(description) }, data: { name, description: parseStringNullish(description) }
}) })
const channel = await prisma.channel.create({ const channel = await prisma.channel.create({
data: { name: "general", guildId: guild.id }, data: { name: 'general', guildId: guild.id }
}) })
const memberCreated = await prisma.member.create({ const memberCreated = await prisma.member.create({
data: { data: {
userId: request.user.current.id, userId: request.user.current.id,
isOwner: true, isOwner: true,
guildId: guild.id, guildId: guild.id
}, }
}) })
const members = await Promise.all( const members = await Promise.all(
[memberCreated].map(async (member) => { [memberCreated].map(async (member) => {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: member?.userId }, where: { id: member?.userId }
}) })
return { return {
...member, ...member,
user, user
} }
}), })
) )
reply.statusCode = 201 reply.statusCode = 201
return { return {
guild: { guild: {
...guild, ...guild,
channels: [channel], channels: [channel],
members, members
}, }
}
} }
},
}) })
} }

View File

@ -1,41 +1,39 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { guildExample } from '../../../../models/Guild.js'
import { application } from "#src/application.js" await tap.test('GET /guilds/public', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { guildExample } from "#src/models/Guild.js"
await test("GET /guilds/public", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
findMany: async () => { findMany: async () => {
return [guildExample] return [guildExample]
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
count: async () => { count: async () => {
return 2 return 2
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: "/guilds/public", url: '/guilds/public',
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.length, 1) t.equal(responseJson.length, 1)
assert.strictEqual(responseJson[0].name, guildExample.name) t.equal(responseJson[0].name, guildExample.name)
assert.strictEqual(responseJson[0].membersCount, 2) t.equal(responseJson[0].membersCount, 2)
}) })
}) })

View File

@ -1,45 +1,44 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../models/Guild.js'
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationSchema, queryPaginationSchema
} from "#src/tools/database/pagination.js" } from '../../../tools/database/pagination.js'
const querySchema = Type.Object({ const querySchema = Type.Object({
search: Type.Optional(Type.String()), search: Type.Optional(Type.String()),
...queryPaginationSchema, ...queryPaginationSchema
}) })
export type QuerySchemaType = Static<typeof querySchema> export type QuerySchemaType = Static<typeof querySchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: description:
"GET all the public guilds (ordered by descending members count).", 'GET all the public guilds (ordered by descending members count).',
tags: ["guilds"] as string[], tags: ['guilds'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
querystring: querySchema, querystring: querySchema,
response: { response: {
200: Type.Array( 200: Type.Array(
Type.Object({ Type.Object({
...guildSchema, ...guildSchema,
membersCount: Type.Integer(), membersCount: Type.Integer()
}), })
), ),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const getGuildsPublic: FastifyPluginAsync = async (fastify) => { export const getGuildsPublic: FastifyPluginAsync = async (fastify) => {
@ -48,8 +47,8 @@ export const getGuildsPublic: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: "GET", method: 'GET',
url: "/guilds/public", url: '/guilds/public',
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -59,28 +58,28 @@ export const getGuildsPublic: FastifyPluginAsync = async (fastify) => {
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
orderBy: { orderBy: {
members: { members: {
_count: "desc", _count: 'desc'
}, }
}, },
...(request.query.search != null && { ...(request.query.search != null && {
where: { where: {
name: { contains: request.query.search }, name: { contains: request.query.search }
}, }
}), })
}) })
const guilds = await Promise.all( const guilds = await Promise.all(
guildsRequest.map(async (guild) => { guildsRequest.map(async (guild) => {
const membersCount = await prisma.member.count({ const membersCount = await prisma.member.count({
where: { guildId: guild.id }, where: { guildId: guild.id }
}) })
return { return {
...guild, ...guild,
membersCount, membersCount
} }
}), })
) )
reply.statusCode = 200 reply.statusCode = 200
return guilds return guilds
}, }
}) })
} }

View File

@ -1,9 +1,9 @@
import type { FastifyPluginAsync } from "fastify" import { FastifyPluginAsync } from 'fastify'
import { usersService } from "./users/index.js" import { usersService } from './users/index.js'
import { guildsService } from "./guilds/index.js" import { guildsService } from './guilds/index.js'
import { channelsService } from "./channels/index.js" import { channelsService } from './channels/index.js'
import { messagesService } from "./messages/index.js" import { messagesService } from './messages/index.js'
export const services: FastifyPluginAsync = async (fastify) => { export const services: FastifyPluginAsync = async (fastify) => {
await fastify.register(channelsService) await fastify.register(channelsService)

View File

@ -1,129 +1,127 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { messageExample } from '../../../../models/Message.js'
import { memberExample } from '../../../../models/Member.js'
import { userExample } from '../../../../models/User.js'
import { channelExample } from '../../../../models/Channel.js'
import { application } from "#src/application.js" await tap.test('DELETE /messsages/[messageId]', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { messageExample } from "#src/models/Message.js"
import { memberExample } from "#src/models/Member.js"
import { userExample } from "#src/models/User.js"
import { channelExample } from "#src/models/Channel.js"
await test("DELETE /messsages/[messageId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample, channel: channelExample
} }
}, },
delete: async () => { delete: async () => {
return messageExample return messageExample
}, }
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample, user: userExample
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.id, messageExample.id) t.equal(responseJson.id, messageExample.id)
assert.strictEqual(responseJson.value, messageExample.value) t.equal(responseJson.value, messageExample.value)
assert.strictEqual(responseJson.type, messageExample.type) t.equal(responseJson.type, messageExample.type)
assert.strictEqual(responseJson.mimetype, messageExample.mimetype) t.equal(responseJson.mimetype, messageExample.mimetype)
assert.strictEqual(responseJson.member.id, memberExample.id) t.equal(responseJson.member.id, memberExample.id)
assert.strictEqual(responseJson.member.isOwner, memberExample.isOwner) t.equal(responseJson.member.isOwner, memberExample.isOwner)
assert.strictEqual(responseJson.member.user.id, userExample.id) t.equal(responseJson.member.user.id, userExample.id)
assert.strictEqual(responseJson.member.user.name, userExample.name) t.equal(responseJson.member.user.name, userExample.name)
}) })
await t.test("fails if the message is not found", async () => { await t.test('fails if the message is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is not found", async () => { await t.test('fails if the member is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample, channel: channelExample
}
} }
},
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is not owner of the message", async () => { await t.test('fails if the member is not owner of the message', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const randomUserIdOwnerOfMessage = 14 const randomUserIdOwnerOfMessage = 14
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample, channel: channelExample
}
} }
},
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
userId: randomUserIdOwnerOfMessage, userId: randomUserIdOwnerOfMessage
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "DELETE", method: 'DELETE',
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, }
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}) })
}) })

View File

@ -1,143 +1,141 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { messageExample } from '../../../../models/Message.js'
import { memberExample } from '../../../../models/Member.js'
import { userExample } from '../../../../models/User.js'
import { channelExample } from '../../../../models/Channel.js'
import { application } from "#src/application.js" await tap.test('PUT /messsages/[messageId]', async (t) => {
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { messageExample } from "#src/models/Message.js"
import { memberExample } from "#src/models/Member.js"
import { userExample } from "#src/models/User.js"
import { channelExample } from "#src/models/Channel.js"
await test("PUT /messsages/[messageId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const newValue = "some message" const newValue = 'some message'
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample, channel: channelExample
} }
}, },
update: async () => { update: async () => {
return { return {
...messageExample, ...messageExample,
value: newValue, value: newValue
}
} }
},
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample, user: userExample
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { value: newValue }, payload: { value: newValue }
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.id, messageExample.id) t.equal(responseJson.id, messageExample.id)
assert.strictEqual(responseJson.value, newValue) t.equal(responseJson.value, newValue)
assert.strictEqual(responseJson.type, messageExample.type) t.equal(responseJson.type, messageExample.type)
assert.strictEqual(responseJson.mimetype, messageExample.mimetype) t.equal(responseJson.mimetype, messageExample.mimetype)
assert.strictEqual(responseJson.member.id, memberExample.id) t.equal(responseJson.member.id, memberExample.id)
assert.strictEqual(responseJson.member.isOwner, memberExample.isOwner) t.equal(responseJson.member.isOwner, memberExample.isOwner)
assert.strictEqual(responseJson.member.user.id, userExample.id) t.equal(responseJson.member.user.id, userExample.id)
assert.strictEqual(responseJson.member.user.name, userExample.name) t.equal(responseJson.member.user.name, userExample.name)
}) })
await t.test("fails if the message is not found", async () => { await t.test('fails if the message is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const newValue = "some message" const newValue = 'some message'
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { value: newValue }, payload: { value: newValue }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test("fails if the member is not found", async () => { await t.test('fails if the member is not found', async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const newValue = "some message" const newValue = 'some message'
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample, channel: channelExample
}
} }
},
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { value: newValue }, payload: { value: newValue }
}) })
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
}) })
await t.test( await t.test(
"fails if the member is not the owner of the message", 'fails if the member is not the owner of the message',
async () => { async (t) => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const newValue = "some message" const newValue = 'some message'
const randomUserIdOwnerOfMessage = 14 const randomUserIdOwnerOfMessage = 14
sinon.stub(prisma, "message").value({ sinon.stub(prisma, 'message').value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample, channel: channelExample
}
} }
},
}) })
sinon.stub(prisma, "member").value({ sinon.stub(prisma, 'member').value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
userId: randomUserIdOwnerOfMessage, userId: randomUserIdOwnerOfMessage
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: "PUT", method: 'PUT',
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}`, authorization: `Bearer ${accessToken}`
}, },
payload: { value: newValue }, payload: { value: newValue }
}) })
assert.strictEqual(response.statusCode, 400) t.equal(response.statusCode, 400)
}, }
) )
}) })

View File

@ -1,27 +1,26 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { messageSchema } from "#src/models/Message.js" import { messageSchema } from '../../../models/Message.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" import { userPublicWithoutSettingsSchema } from '../../../models/User.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
messageId: messageSchema.id, messageId: messageSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: "UPDATE a message with its id.", description: 'UPDATE a message with its id.',
tags: ["messages"] as string[], tags: ['messages'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -29,15 +28,15 @@ const putServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), })
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const deleteMessageService: FastifyPluginAsync = async (fastify) => { export const deleteMessageService: FastifyPluginAsync = async (fastify) => {
@ -46,28 +45,28 @@ export const deleteMessageService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: "DELETE", method: 'DELETE',
url: "/messages/:messageId", url: '/messages/:messageId',
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user, params } = request const { user } = request
const { messageId } = params const { messageId } = request.params
const messageCheck = await prisma.message.findFirst({ const messageCheck = await prisma.message.findFirst({
where: { id: messageId }, where: { id: messageId },
include: { include: {
channel: true, channel: true
}, }
}) })
if (messageCheck == null || messageCheck.channel == null) { if (messageCheck == null || messageCheck.channel == null) {
throw fastify.httpErrors.notFound("Message not found") throw fastify.httpErrors.notFound('Message not found')
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { where: {
guildId: messageCheck.channel.guildId, guildId: messageCheck.channel.guildId,
userId: user.current.id, userId: user.current.id
}, },
include: { include: {
user: { user: {
@ -79,23 +78,23 @@ export const deleteMessageService: FastifyPluginAsync = async (fastify) => {
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
}, }
}, }
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
if (member.userId !== user.current.id) { if (member.userId !== user.current.id) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
"You should be the owner of the message", 'You should be the owner of the message'
) )
} }
const message = await prisma.message.delete({ const message = await prisma.message.delete({
where: { where: {
id: messageCheck.id, id: messageCheck.id
}, }
}) })
const item = { const item = {
...message, ...message,
@ -103,17 +102,17 @@ export const deleteMessageService: FastifyPluginAsync = async (fastify) => {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null, email: null
}, }
}, }
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "messages", event: 'messages',
guildId: item.member.guildId, guildId: item.member.guildId,
payload: { action: "delete", item }, payload: { action: 'delete', item }
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
}, }
}) })
} }

View File

@ -1,33 +1,32 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import authenticateUser from "#src/tools/plugins/authenticateUser.js" import authenticateUser from '../../../tools/plugins/authenticateUser.js'
import { messageSchema } from "#src/models/Message.js" import { messageSchema } from '../../../models/Message.js'
import { memberSchema } from "#src/models/Member.js" import { memberSchema } from '../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from "#src/models/User.js" import { userPublicWithoutSettingsSchema } from '../../../models/User.js'
const bodyPutServiceSchema = Type.Object({ const bodyPutServiceSchema = Type.Object({
value: messageSchema.value, value: messageSchema.value
}) })
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema> type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
messageId: messageSchema.id, messageId: messageSchema.id
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: "UPDATE a message with its id.", description: 'UPDATE a message with its id.',
tags: ["messages"] as string[], tags: ['messages'] as string[],
security: [ security: [
{ {
bearerAuth: [], bearerAuth: []
}, }
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPutServiceSchema, body: bodyPutServiceSchema,
params: parametersSchema, params: parametersSchema,
@ -36,15 +35,15 @@ const putServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema), user: Type.Object(userPublicWithoutSettingsSchema)
}), })
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const putMessageService: FastifyPluginAsync = async (fastify) => { export const putMessageService: FastifyPluginAsync = async (fastify) => {
@ -54,29 +53,29 @@ export const putMessageService: FastifyPluginAsync = async (fastify) => {
Body: BodyPutServiceSchemaType Body: BodyPutServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: "PUT", method: 'PUT',
url: "/messages/:messageId", url: '/messages/:messageId',
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user, params, body } = request const { user } = request
const { messageId } = params const { messageId } = request.params
const { value } = body const { value } = request.body
const messageCheck = await prisma.message.findFirst({ const messageCheck = await prisma.message.findFirst({
where: { id: messageId, type: "text" }, where: { id: messageId, type: 'text' },
include: { include: {
channel: true, channel: true
}, }
}) })
if (messageCheck == null || messageCheck.channel == null) { if (messageCheck == null || messageCheck.channel == null) {
throw fastify.httpErrors.notFound("Message not found") throw fastify.httpErrors.notFound('Message not found')
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { where: {
guildId: messageCheck.channel.guildId, guildId: messageCheck.channel.guildId,
userId: user.current.id, userId: user.current.id
}, },
include: { include: {
user: { user: {
@ -88,26 +87,26 @@ export const putMessageService: FastifyPluginAsync = async (fastify) => {
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
}, }
}, }
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound("Member not found") throw fastify.httpErrors.notFound('Member not found')
} }
if (member.userId !== user.current.id) { if (member.userId !== user.current.id) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
"You should be the owner of the message", 'You should be the owner of the message'
) )
} }
const message = await prisma.message.update({ const message = await prisma.message.update({
where: { where: {
id: messageCheck.id, id: messageCheck.id
}, },
data: { data: {
value, value
}, }
}) })
const item = { const item = {
...message, ...message,
@ -115,17 +114,17 @@ export const putMessageService: FastifyPluginAsync = async (fastify) => {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null, email: null
}, }
}, }
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: "messages", event: 'messages',
guildId: item.member.guildId, guildId: item.member.guildId,
payload: { action: "update", item }, payload: { action: 'update', item }
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
}, }
}) })
} }

View File

@ -1,7 +1,7 @@
import type { FastifyPluginAsync } from "fastify" import { FastifyPluginAsync } from 'fastify'
import { deleteMessageService } from "./[messageId]/delete.js" import { deleteMessageService } from './[messageId]/delete.js'
import { putMessageService } from "./[messageId]/put.js" import { putMessageService } from './[messageId]/put.js'
export const messagesService: FastifyPluginAsync = async (fastify) => { export const messagesService: FastifyPluginAsync = async (fastify) => {
await fastify.register(putMessageService) await fastify.register(putMessageService)

View File

@ -1,56 +1,54 @@
import test from "node:test" import tap from 'tap'
import assert from "node:assert/strict" import sinon from 'sinon'
import sinon from "sinon" import { application } from '../../../../application.js'
import prisma from '../../../../tools/database/prisma.js'
import { userExample } from '../../../../models/User.js'
import { userSettingsExample } from '../../../../models/UserSettings.js'
import { application } from "#src/application.js" await tap.test('GET /users/[userId]', async (t) => {
import prisma from "#src/tools/database/prisma.js"
import { userExample } from "#src/models/User.js"
import { userSettingsExample } from "#src/models/UserSettings.js"
await test("GET /users/[userId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test("succeeds", async () => { await t.test('succeeds', async (t) => {
sinon.stub(prisma, "guild").value({ sinon.stub(prisma, 'guild').value({
findMany: async () => { findMany: async () => {
return [] return []
}, }
}) })
sinon.stub(prisma, "user").value({ sinon.stub(prisma, 'user').value({
findUnique: async () => { findUnique: async () => {
return userExample return userExample
}, }
}) })
sinon.stub(prisma, "userSetting").value({ sinon.stub(prisma, 'userSetting').value({
findFirst: async () => { findFirst: async () => {
return userSettingsExample return userSettingsExample
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/users/${userExample.id}`, url: `/users/${userExample.id}`
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 200) t.equal(response.statusCode, 200)
assert.strictEqual(responseJson.user.id, userExample.id) t.equal(responseJson.user.id, userExample.id)
assert.strictEqual(responseJson.user.name, userExample.name) t.equal(responseJson.user.name, userExample.name)
}) })
await t.test("fails with not found user", async () => { await t.test('fails with not found user', async (t) => {
sinon.stub(prisma, "userSetting").value({ sinon.stub(prisma, 'userSetting').value({
findFirst: async () => { findFirst: async () => {
return null return null
}, }
}) })
const response = await application.inject({ const response = await application.inject({
method: "GET", method: 'GET',
url: `/users/1`, url: `/users/1`
}) })
const responseJson = response.json() const responseJson = response.json()
assert.strictEqual(response.statusCode, 404) t.equal(response.statusCode, 404)
assert.strictEqual(responseJson.message, "User not found") t.equal(responseJson.message, 'User not found')
}) })
}) })

View File

@ -1,74 +1,72 @@
import type { Static } from "@sinclair/typebox" import { Static, Type } from '@sinclair/typebox'
import { Type } from "@sinclair/typebox" import { FastifyPluginAsync, FastifySchema } from 'fastify'
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from "#src/tools/database/prisma.js" import prisma from '../../../tools/database/prisma.js'
import { fastifyErrors } from "#src/models/utils.js" import { fastifyErrors } from '../../../models/utils.js'
import { userPublicSchema } from "#src/models/User.js" import { userPublicSchema } from '../../../models/User.js'
import { guildSchema } from "#src/models/Guild.js" import { guildSchema } from '../../../models/Guild.js'
const parametersGetUserSchema = Type.Object({ const parametersGetUserSchema = Type.Object({
userId: userPublicSchema.id, userId: userPublicSchema.id
}) })
type ParametersGetUser = Static<typeof parametersGetUserSchema> type ParametersGetUser = Static<typeof parametersGetUserSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: "GET the public user informations with its id", description: 'GET the public user informations with its id',
tags: ["users"] as string[], tags: ['users'] as string[],
params: parametersGetUserSchema, params: parametersGetUserSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
user: Type.Object(userPublicSchema), user: Type.Object(userPublicSchema),
guilds: Type.Array(Type.Object(guildSchema)), guilds: Type.Array(Type.Object(guildSchema))
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500], 500: fastifyErrors[500]
}, }
} as const } as const
export const getUserById: FastifyPluginAsync = async (fastify) => { export const getUserById: FastifyPluginAsync = async (fastify) => {
await fastify.route<{ await fastify.route<{
Params: ParametersGetUser Params: ParametersGetUser
}>({ }>({
method: "GET", method: 'GET',
url: "/users/:userId", url: '/users/:userId',
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
const { userId } = request.params const { userId } = request.params
const settings = await prisma.userSetting.findFirst({ const settings = await prisma.userSetting.findFirst({
where: { userId }, where: { userId }
}) })
if (settings == null) { if (settings == null) {
throw fastify.httpErrors.notFound("User not found") throw fastify.httpErrors.notFound('User not found')
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: userId, id: userId
}, },
select: { select: {
id: true, id: true,
name: true, name: true,
email: settings.isPublicEmail, email: settings.isPublicEmail,
isConfirmed: true,
logo: true, logo: true,
status: true, status: true,
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true, updatedAt: true
}, }
}) })
if (user == null) { if (user == null) {
throw fastify.httpErrors.notFound("User not found") throw fastify.httpErrors.notFound('User not found')
} }
reply.statusCode = 200 reply.statusCode = 200
return { return {
user: { user: {
...user, ...user,
email: user.email ?? null, email: user.email ?? null,
settings, settings
}, },
guilds: !settings.isPublicGuilds guilds: !settings.isPublicGuilds
? [] ? []
@ -77,12 +75,12 @@ export const getUserById: FastifyPluginAsync = async (fastify) => {
where: { where: {
members: { members: {
some: { some: {
userId, userId
}, }
}, }
}, }
}), })
}
} }
},
}) })
} }

Some files were not shown because too many files have changed in this diff Show More