Repository: sentrionic/Valkyrie Branch: main Commit: 5daec722cd19 Files: 291 Total size: 1.4 MB Directory structure: gitextract_j1xfpjp2/ ├── .github/ │ └── workflows/ │ ├── release_build.yml │ ├── server_ci.yml │ ├── server_deploy.yml │ ├── web_ci.yml │ ├── website-e2e.yml │ └── website_deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── server/ │ ├── .gitattributes │ ├── .gitignore │ ├── .idea/ │ │ ├── .gitignore │ │ ├── ValkyrieGo.iml │ │ ├── dataSources.xml │ │ ├── modules.xml │ │ └── vcs.xml │ ├── Makefile │ ├── config/ │ │ └── config.go │ ├── data_sources.go │ ├── docs/ │ │ ├── docs.go │ │ ├── swagger.json │ │ └── swagger.yaml │ ├── e2e_test.go │ ├── go.mod │ ├── go.sum │ ├── handler/ │ │ ├── account_handler.go │ │ ├── account_handler_test.go │ │ ├── auth_handler.go │ │ ├── auth_handler_test.go │ │ ├── bind_data.go │ │ ├── channel_handler.go │ │ ├── channel_handler_test.go │ │ ├── friend_handler.go │ │ ├── friend_handler_test.go │ │ ├── guild_handler.go │ │ ├── guild_handler_test.go │ │ ├── handler.go │ │ ├── member_handler.go │ │ ├── member_handler_test.go │ │ ├── message_handler.go │ │ ├── message_handler_test.go │ │ ├── middleware/ │ │ │ ├── auth_user.go │ │ │ ├── auth_user_test.go │ │ │ └── timeout.go │ │ ├── mime_type.go │ │ └── test_helpers.go │ ├── injection.go │ ├── main.go │ ├── mocks/ │ │ ├── ChannelRepository.go │ │ ├── ChannelService.go │ │ ├── FileRepository.go │ │ ├── FriendRepository.go │ │ ├── FriendService.go │ │ ├── GuildRepository.go │ │ ├── GuildService.go │ │ ├── MailRepository.go │ │ ├── MessageRepository.go │ │ ├── MessageService.go │ │ ├── RedisRepository.go │ │ ├── Request.go │ │ ├── SocketService.go │ │ ├── UserRepository.go │ │ └── UserService.go │ ├── model/ │ │ ├── app_constants.go │ │ ├── apperrors/ │ │ │ ├── apperrors.go │ │ │ └── httperrors.go │ │ ├── base_model.go │ │ ├── channel.go │ │ ├── direct_message.go │ │ ├── dm_member.go │ │ ├── field_error.go │ │ ├── fixture/ │ │ │ ├── channel.go │ │ │ ├── faker.go │ │ │ ├── guild.go │ │ │ ├── message.go │ │ │ ├── multipart.go │ │ │ └── user.go │ │ ├── friend.go │ │ ├── friend_request.go │ │ ├── guild.go │ │ ├── interfaces.go │ │ ├── invite.go │ │ ├── member.go │ │ ├── message.go │ │ ├── user.go │ │ └── ws_message.go │ ├── repository/ │ │ ├── channel_repository.go │ │ ├── file_repository.go │ │ ├── friend_repository.go │ │ ├── guild_repository.go │ │ ├── mail_repository.go │ │ ├── message_repository.go │ │ ├── redis_repository.go │ │ └── user_repository.go │ ├── service/ │ │ ├── channel_service.go │ │ ├── channel_service_test.go │ │ ├── friend_service.go │ │ ├── guild_service.go │ │ ├── guild_service_test.go │ │ ├── id_generator.go │ │ ├── message_service.go │ │ ├── message_service_test.go │ │ ├── password.go │ │ ├── password_test.go │ │ ├── socket_service.go │ │ ├── user_service.go │ │ └── user_service_test.go │ ├── static/ │ │ ├── asyncapi.yml │ │ ├── index.html │ │ └── js/ │ │ └── main.js │ └── ws/ │ ├── actions.go │ ├── client.go │ ├── hub.go │ └── room.go └── web/ ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── cypress/ │ ├── e2e/ │ │ ├── account.cy.ts │ │ ├── channel.cy.ts │ │ ├── friend.cy.ts │ │ ├── guild.cy.ts │ │ ├── member.cy.ts │ │ └── message.cy.ts │ ├── fixtures/ │ │ └── example.json │ ├── plugins/ │ │ └── index.js │ ├── support/ │ │ ├── commands.ts │ │ ├── e2e.js │ │ ├── index.d.ts │ │ └── utils.ts │ └── tsconfig.json ├── cypress.config.ts ├── package.json ├── public/ │ ├── _redirects │ ├── index.html │ ├── manifest.json │ └── robots.txt ├── src/ │ ├── App.tsx │ ├── components/ │ │ ├── common/ │ │ │ ├── GuildPills.tsx │ │ │ ├── InputField.tsx │ │ │ ├── Logo.tsx │ │ │ └── NotificationIcon.tsx │ │ ├── items/ │ │ │ ├── ChannelListItem.tsx │ │ │ ├── DMListItem.tsx │ │ │ ├── FriendsListItem.tsx │ │ │ ├── GuildListItem.tsx │ │ │ ├── MemberListItem.tsx │ │ │ ├── NotificationListItem.tsx │ │ │ ├── RequestListItem.tsx │ │ │ ├── VoiceChannelItem.tsx │ │ │ ├── css/ │ │ │ │ └── ContextMenu.css │ │ │ └── message/ │ │ │ ├── Message.tsx │ │ │ └── MessageContent.tsx │ │ ├── layouts/ │ │ │ ├── AccountBar.tsx │ │ │ ├── AppLayout.tsx │ │ │ ├── LandingLayout.tsx │ │ │ ├── VoiceBar.tsx │ │ │ ├── guild/ │ │ │ │ ├── ChannelHeader.tsx │ │ │ │ ├── Channels.tsx │ │ │ │ ├── GuildList.tsx │ │ │ │ ├── MemberList.tsx │ │ │ │ ├── VoiceChat.tsx │ │ │ │ ├── chat/ │ │ │ │ │ ├── ChatGrid.tsx │ │ │ │ │ ├── ChatScreen.tsx │ │ │ │ │ ├── FileUploadButton.tsx │ │ │ │ │ └── MessageInput.tsx │ │ │ │ └── css/ │ │ │ │ ├── ChannelScrollerCSS.ts │ │ │ │ ├── GuildScrollerCSS.ts │ │ │ │ ├── MemberScrollerCSS.ts │ │ │ │ └── MessageInput.css │ │ │ └── home/ │ │ │ ├── DMHeader.tsx │ │ │ ├── DMSidebar.tsx │ │ │ ├── css/ │ │ │ │ └── dmScrollerCSS.ts │ │ │ └── dashboard/ │ │ │ ├── FriendsDashboard.tsx │ │ │ ├── FriendsList.tsx │ │ │ ├── FriendsListHeader.tsx │ │ │ └── PendingList.tsx │ │ ├── menus/ │ │ │ ├── GuildMenu.tsx │ │ │ ├── MemberContextMenu.tsx │ │ │ ├── StyledMenuItem.tsx │ │ │ └── StyledMenuList.tsx │ │ ├── modals/ │ │ │ ├── AddFriendModal.tsx │ │ │ ├── AddGuildModal.tsx │ │ │ ├── ChangePasswordModal.tsx │ │ │ ├── ChannelSettingsModal.tsx │ │ │ ├── CreateChannelModal.tsx │ │ │ ├── CropImageModal.tsx │ │ │ ├── DeleteMessageModal.tsx │ │ │ ├── EditMemberModal.tsx │ │ │ ├── EditMessageModal.tsx │ │ │ ├── GuildSettingsModal.tsx │ │ │ ├── InviteModal.tsx │ │ │ ├── ModActionModal.tsx │ │ │ └── RemoveFriendModal.tsx │ │ └── sections/ │ │ ├── AddGuildIcon.tsx │ │ ├── DMPlaceholder.tsx │ │ ├── DateDivider.tsx │ │ ├── Footer.tsx │ │ ├── FriendsListButton.tsx │ │ ├── GlobalState.tsx │ │ ├── Hero.tsx │ │ ├── HomeIcon.tsx │ │ ├── NavBar.tsx │ │ ├── OnlineLabel.tsx │ │ ├── StartMessages.tsx │ │ ├── StyledTooltip.tsx │ │ └── UserPopover.tsx │ ├── index.tsx │ ├── lib/ │ │ ├── api/ │ │ │ ├── dtos/ │ │ │ │ ├── AuthInput.ts │ │ │ │ ├── ChannelInput.ts │ │ │ │ ├── GuildInput.ts │ │ │ │ ├── GuildMemberInput.ts │ │ │ │ ├── InviteInput.ts │ │ │ │ └── UserInput.ts │ │ │ ├── getSocket.ts │ │ │ ├── handler/ │ │ │ │ ├── account.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── channel.ts │ │ │ │ ├── dm.ts │ │ │ │ ├── guilds.ts │ │ │ │ ├── members.ts │ │ │ │ └── messages.ts │ │ │ ├── setupAxios.ts │ │ │ └── ws/ │ │ │ ├── useChannelSocket.ts │ │ │ ├── useDMSocket.ts │ │ │ ├── useFriendSocket.ts │ │ │ ├── useGuildSocket.ts │ │ │ ├── useMemberSocket.ts │ │ │ ├── useMessageSocket.ts │ │ │ ├── useRequestSocket.ts │ │ │ └── useVoiceSocket.ts │ │ ├── models/ │ │ │ ├── account.ts │ │ │ ├── channel.ts │ │ │ ├── dm.ts │ │ │ ├── fieldError.ts │ │ │ ├── friend.ts │ │ │ ├── guild.ts │ │ │ ├── member.ts │ │ │ ├── message.ts │ │ │ ├── routerProps.ts │ │ │ └── voice.ts │ │ ├── stores/ │ │ │ ├── channelStore.ts │ │ │ ├── homeStore.ts │ │ │ ├── settingsStore.ts │ │ │ ├── userStore.ts │ │ │ └── voiceStore.ts │ │ └── utils/ │ │ ├── cropImage.ts │ │ ├── dateUtils.ts │ │ ├── hooks/ │ │ │ ├── useGetCurrentChannel.ts │ │ │ ├── useGetCurrentDM.ts │ │ │ ├── useGetCurrentGuild.ts │ │ │ ├── useGetFriend.ts │ │ │ └── useVoiceChat.ts │ │ ├── querykeys.ts │ │ ├── theme.ts │ │ ├── toErrorMap.ts │ │ └── validation/ │ │ ├── auth.schema.ts │ │ ├── channel.schema.ts │ │ ├── guild.schema.ts │ │ ├── member.schema.ts │ │ └── message.schema.ts │ ├── react-app-env.d.ts │ ├── routes/ │ │ ├── AuthRoute.tsx │ │ ├── ForgotPassword.tsx │ │ ├── Home.tsx │ │ ├── Invite.tsx │ │ ├── Landing.tsx │ │ ├── Login.tsx │ │ ├── Register.tsx │ │ ├── ResetPassword.tsx │ │ ├── Routes.tsx │ │ ├── Settings.tsx │ │ └── ViewGuild.tsx │ ├── setupTests.ts │ └── tests/ │ ├── fixture/ │ │ ├── accountFixture.ts │ │ ├── channelFixtures.ts │ │ ├── dmFixtures.ts │ │ ├── friendFixture.ts │ │ ├── guildFixtures.ts │ │ ├── memberFixtures.ts │ │ ├── messageFixtures.ts │ │ └── requestFixtures.ts │ ├── queries/ │ │ ├── account.test.tsx │ │ ├── channel.test.tsx │ │ ├── dm.test.tsx │ │ ├── friend.test.tsx │ │ ├── guild.test.tsx │ │ ├── member.test.tsx │ │ ├── message.test.tsx │ │ └── request.test.tsx │ └── testUtils.tsx └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/release_build.yml ================================================ name: Release Server Binary on: release: types: [created] jobs: releases-matrix: name: Release Go Binary runs-on: ubuntu-latest strategy: matrix: goos: [linux, windows, darwin] goarch: ['386', amd64] exclude: - goarch: '386' goos: darwin steps: - uses: actions/checkout@v3 - uses: wangyoucao577/go-release-action@v1.38 with: github_token: ${{ secrets.GITHUB_TOKEN }} goos: ${{ matrix.goos }} goarch: ${{ matrix.goarch }} project_path: './server' binary_name: 'valkyrie-server' md5sum: false extra_files: ./server/.env.example README.md ================================================ FILE: .github/workflows/server_ci.yml ================================================ name: Test & Lint on: push: paths: - 'server/**' pull_request: paths: - 'server/**' jobs: golangci: name: lint runs-on: ubuntu-latest steps: - uses: actions/setup-go@v4 with: go-version: 1.20 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: version: latest args: --timeout=5m working-directory: ./server test: name: Test runs-on: ubuntu-latest services: postgres: image: postgres:alpine env: POSTGRES_USER: root POSTGRES_PASSWORD: secret POSTGRES_DB: valkyrie ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: 'redis:alpine' ports: - '6379:6379' volumes: - 'redisdata:/data' steps: - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.20 id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Run Unit Tests working-directory: ./server run: make test - name: Run E2E run: make e2e working-directory: ./server env: DATABASE_URL: postgresql://root:secret@localhost:5432/valkyrie?sslmode=disable HANDLER_TIMEOUT: 5 MAX_BODY_BYTES: 4194304 REDIS_URL: redis://localhost:6379 SECRET: jmaijopspahisodphiasdhiahiopsdhoiasdg8a89sdta08sdtg8aosdou CORS_ORIGIN: origin ================================================ FILE: .github/workflows/server_deploy.yml ================================================ # name: Deploy [API] # on: # workflow_run: # workflows: ['Test & Lint'] # branches: [main] # types: [completed] # jobs: # on-success: # runs-on: ubuntu-latest # if: ${{ github.event.workflow_run.conclusion == 'success' }} # steps: # - uses: actions/checkout@v3 # - uses: akhileshns/heroku-deploy@v3.12.12 # with: # heroku_api_key: ${{secrets.HEROKU_API_KEY}} # heroku_app_name: ${{secrets.HEROKU_APP_NAME}} # heroku_email: ${{secrets.HEROKU_EMAIL}} # appdir: 'server' ================================================ FILE: .github/workflows/web_ci.yml ================================================ name: Test & Lint - Web on: push: paths: - 'web/**' pull_request: paths: - 'web/**' jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js 20.x uses: actions/setup-node@v3 with: node-version: '20.x' cache: 'yarn' cache-dependency-path: web/yarn.lock - run: cd web && yarn install - run: cd web && yarn compile - run: cd web && yarn lint - run: cd web && yarn test --watchAll=false ================================================ FILE: .github/workflows/website-e2e.yml ================================================ name: Website E2E on: workflow_run: workflows: ['Test & Lint - Web'] branches: [main] types: [completed] jobs: cypress-run: runs-on: ubuntu-latest services: postgres: image: postgres:alpine env: POSTGRES_USER: root POSTGRES_PASSWORD: secret POSTGRES_DB: valkyrie ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 redis: image: 'redis:alpine' ports: - '6379:6379' volumes: - 'redisdata:/data' steps: - name: Set up Go uses: actions/setup-go@v4 with: go-version: 1.20 id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Run Server run: go run github.com/sentrionic/valkyrie & working-directory: ./server env: DATABASE_URL: postgresql://root:secret@localhost:5432/valkyrie?sslmode=disable HANDLER_TIMEOUT: 5 MAX_BODY_BYTES: 4194304 REDIS_URL: redis://localhost:6379 SECRET: jmaijopspahisodphiasdhiahiopsdhoiasdg8a89sdta08sdtg8aosdou PORT: 4000 CORS_ORIGIN: http://localhost:3000 GIN_MODE: release - name: Cypress run uses: cypress-io/github-action@v4 with: install-command: yarn start: yarn start wait-on: http://localhost:3000 browser: chrome working-directory: ./web headless: true env: CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REACT_APP_API: http://localhost:4000 REACT_APP_WS: ws://http://localhost:4000/ws ================================================ FILE: .github/workflows/website_deploy.yml ================================================ # name: Website Deploy # on: # workflow_run: # workflows: ['Website E2E'] # branches: [main] # types: [completed] # jobs: # on-success: # runs-on: ubuntu-latest # if: ${{ github.event.workflow_run.conclusion == 'success' }} # steps: # - name: Checkout code # uses: actions/checkout@v3 # - name: Use Node.js 18.x # uses: actions/setup-node@v3 # with: # node-version: '18.x' # cache: 'yarn' # cache-dependency-path: web/yarn.lock # - run: yarn install # working-directory: ./web # - run: yarn build --if-present # working-directory: ./web # - name: Deploy to netlify # uses: netlify/actions/cli@master # env: # NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} # NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} # with: # args: deploy --dir=web/build --prod # secrets: '["NETLIFY_AUTH_TOKEN", "NETLIFY_SITE_ID"]' ================================================ FILE: .gitignore ================================================ # Created by https://www.toptal.com/developers/gitignore/api/webstorm # Edit at https://www.toptal.com/developers/gitignore?templates=webstorm ### WebStorm ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### WebStorm Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 # *.iml # modules.xml # .idea/misc.xml # *.ipr # Sonarlint plugin # https://plugins.jetbrains.com/plugin/7973-sonarlint .idea/**/sonarlint/ # SonarQube Plugin # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin .idea/**/sonarIssues.xml # Markdown Navigator plugin # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced .idea/**/markdown-navigator.xml .idea/**/markdown-navigator-enh.xml .idea/**/markdown-navigator/ # Cache file creation bug # See https://youtrack.jetbrains.com/issue/JBR-2257 .idea/$CACHE_FILE$ # CodeStream plugin # https://plugins.jetbrains.com/plugin/12206-codestream .idea/codestream.xml # End of https://www.toptal.com/developers/gitignore/api/webstorm # Local Netlify folder .netlify ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 sentrionic Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![Go Report Card](https://goreportcard.com/badge/github.com/sentrionic/Valkyrie)](https://goreportcard.com/report/github.com/sentrionic/Valkyrie) # Valkyrie

A [Discord](https://discord.com) clone using [React](https://reactjs.org/) and [Go](https://golang.org/). **Notes:** - The design does not fully match the current design of Discord anymore. - For the old [Socket.io](https://socket.io/) stack using [NestJS](https://nestjs.com/) check out the [v1](https://github.com/sentrionic/Valkyrie/tree/v1) branch. ## Video https://user-images.githubusercontent.com/38354571/137365365-a7fe91d6-51d7-4739-8742-f68517223f8f.mp4 ## Features - Message, Channel, Server CRUD - Authentication using Express Sessions - Channel / Websocket Member Protection - Realtime Events - File Upload (Avatar, Icon, Messages) to S3 - Direct Messaging - Private Channels - Friend System - Notification System - Basic Moderation for the guild owner (delete messages, kick & ban members) - Basic Voice Chat (one voice channel per guild + mute & deafen) ## Stack ### Server - [Gin](https://gin-gonic.com/) for the HTTP server - [Gorilla Websockets](https://github.com/gorilla/websocket) for WS communication - [Gorm](https://gorm.io/) as the database ORM - PostgreSQL to save all data - Redis for storing sessions and reset tokens - S3 for storing files and Gmail for sending emails ### Web - React with [Chakra UI](https://chakra-ui.com/) - [React Query](https://react-query.tanstack.com/) & [Zustand](https://github.com/pmndrs/zustand) for state management - [Typescript](https://www.typescriptlang.org/) For the mobile app using Flutter check out [ValkyrieApp](https://github.com/sentrionic/ValkyrieApp) --- ## Installation ### Server If you are familiar with `make`, take a look at the `Makefile` to quickly setup the following steps or alternatively copy the commands into your CLI. 1. Install Docker and get the Postgresql and Redis containers (`make postgres` && `make redis`) 2. Start both containers (`make start`) and create a DB (`make createdb`) 3. Install the latest version of Go and get all the dependencies (`go mod tidy`) 4. Rename `.env.example` to `.env` and fill in the values - `Required` PORT=4000 DATABASE_URL=postgresql://:@localhost:5432/valkyrie REDIS_URL=redis://localhost:6379 CORS_ORIGIN=http://localhost:3000 SECRET=SUPERSECRET HANDLER_TIMEOUT=5 MAX_BODY_BYTES=4194304 # 4MB in Bytes = 4 * 1024 * 1024 - `Optional: Not needed to run the app, but you won't be able to upload files or send emails.` AWS_ACCESS_KEY=ACCESS_KEY AWS_SECRET_ACCESS_KEY=SECRET_ACCESS_KEY AWS_STORAGE_BUCKET_NAME=STORAGE_BUCKET_NAME AWS_S3_REGION=S3_REGION GMAIL_USER=GMAIL_USER GMAIL_PASSWORD=GMAIL_PASSWORD 5. Run `go run github.com/sentrionic/valkyrie` to run the server **Alternatively**: If you only want to run the backend without installing Go and all dependencies, you can download the pre compiled server from the [Release tab](https://github.com/sentrionic/Valkyrie/releases) instead. You will still need to follow the above steps 1, 2 and 4. ### Web 1. Install Node 20 or the LTS version of Node. 2. Install [yarn](https://classic.yarnpkg.com/lang/en/) 3. Run `yarn` to install the dependencies 4. Run `yarn start` to start the client 5. Go to `localhost:3000` ## Endpoints Once the server is running go to `localhost:/swagger/index.html` to see all the HTTP endpoints and `localhost:` for all the websockets events. ## Tests All tests are run on all push and pull requests. Only if they are successful it will run the other Github Actions to automatically deploy the updates. ### Server All routes in `handler` have tests written for them. Function calls in the `service` directory that do not just delegate work to the repository have tests written for them. Run `go test -v -cover ./service/... ./handler/...` (`make test`) to run all tests Additionally this repository includes E2E tests for all successful requests. To run them you have to have Postgres and Redis running in Docker and then run `go test github.com/sentrionic/valkyrie` (`make e2e`). ### Web Most `useQuery` hooks have tests written for them. To run them use `yarn test`. Additionally [Cypress](https://www.cypress.io/) is used for E2E testing. To run them you need to have the server and the client running. After that run `yarn cypress` to open the test window. **Note**: For unkown reasons websockets connection only randomly work during Cypress runs, which makes testing them impossible. ## Credits [Ben Awad](https://github.com/benawad): The inital project is based on his Slack tutorial series and I always look at his repositories for inspiration. [Jacob Goodwin](https://github.com/JacobSNGoodwin/memrizr): This backend is built upon his tutorial series and uses his backend structure. [Jeroen de Kok](https://dev.to/jeroendk/building-a-simple-chat-application-with-websockets-in-go-and-vue-js-gao): The websockets structure is based on his tutorial. [ericellb](https://github.com/ericellb/React-Discord-Clone): His repository helped me implement voice chat. ================================================ FILE: server/.gitattributes ================================================ *.html -linguist-detectable *.css -linguist-detectable *.js -linguist-detectable ================================================ FILE: server/.gitignore ================================================ # Created by https://www.toptal.com/developers/gitignore/api/go # Edit at https://www.toptal.com/developers/gitignore?templates=go .env ### Go ### # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ ### Go Patch ### /vendor/ /Godeps/ # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # End of https://www.toptal.com/developers/gitignore/api/go ================================================ FILE: server/.idea/.gitignore ================================================ # Default ignored files /shelf/ /workspace.xml # Datasource local storage ignored files /dataSources/ /dataSources.local.xml # Editor-based HTTP Client requests /httpRequests/ ================================================ FILE: server/.idea/ValkyrieGo.iml ================================================ ================================================ FILE: server/.idea/dataSources.xml ================================================ postgresql true org.postgresql.Driver jdbc:postgresql://localhost:5432/valkyrie $ProjectFileDir$ ================================================ FILE: server/.idea/modules.xml ================================================ ================================================ FILE: server/.idea/vcs.xml ================================================ ================================================ FILE: server/Makefile ================================================ postgres: docker run --name postgres -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=password -d postgres:alpine redis: docker run --name redis -d -p 6379:6379 redis:alpine redis-server --save 60 1 createdb: docker exec -it postgres createdb --username=root --owner=root valkyrie dropdb: docker exec -it postgres dropdb valkyrie recreate: make dropdb && make createdb start: docker start postgres && docker start redis test: go test -v -cover ./service/... ./handler/... e2e: go test github.com/sentrionic/valkyrie lint: golangci-lint run mock: mockery --all build: go build github.com/sentrionic/valkyrie fmt: go fmt github.com/sentrionic/... swag: swag init workflow: make fmt && make lint && make test ================================================ FILE: server/config/config.go ================================================ package config import ( "context" "github.com/sethvargo/go-envconfig" ) type Config struct { DatabaseUrl string `env:"DATABASE_URL,required"` RedisUrl string `env:"REDIS_URL,required"` Port string `env:"PORT,default=4000"` SessionSecret string `env:"SECRET,required"` Domain string `env:"DOMAIN"` CorsOrigin string `env:"CORS_ORIGIN,required"` AccessKey string `env:"AWS_ACCESS_KEY"` SecretKey string `env:"SECRET_KEY"` BucketName string `env:"BUCKET_NAME"` Region string `env:"REGION"` GmailUser string `env:"GMAIL_USER"` GmailPassword string `env:"GMAIL_PASSWORD"` HandlerTimeOut int64 `env:"HANDLER_TIMEOUT,default=5"` MaxBodyBytes int64 `env:"MAX_BODY_BYTES,default=4194304"` } func LoadConfig(ctx context.Context) (config Config, err error) { err = envconfig.Process(ctx, &config) if err != nil { return } return } ================================================ FILE: server/data_sources.go ================================================ package main import ( "context" "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/redis/go-redis/v9" "github.com/sentrionic/valkyrie/config" "github.com/sentrionic/valkyrie/model" "gorm.io/driver/postgres" "gorm.io/gorm" "log" ) type dataSources struct { DB *gorm.DB RedisClient *redis.Client S3Session *session.Session } // InitDS establishes connections to fields in dataSources func initDS(ctx context.Context, cfg config.Config) (*dataSources, error) { log.Printf("Initializing data sources\n") log.Printf("Connecting to Postgresql\n") db, err := gorm.Open(postgres.Open(cfg.DatabaseUrl)) if err != nil { return nil, fmt.Errorf("error opening db: %w", err) } // Migrate models and setup join tables if err = db.AutoMigrate( &model.User{}, &model.Guild{}, &model.Member{}, &model.Channel{}, &model.DMMember{}, &model.Message{}, &model.Attachment{}, &model.VCMember{}, ); err != nil { return nil, fmt.Errorf("error migrating models: %w", err) } if err = db.SetupJoinTable(&model.Guild{}, "Members", &model.Member{}); err != nil { return nil, fmt.Errorf("error creating join table: %w", err) } if err = db.SetupJoinTable(&model.Guild{}, "VCMembers", &model.VCMember{}); err != nil { return nil, fmt.Errorf("error creating join table: %w", err) } // Initialize redis connection opt, err := redis.ParseURL(cfg.RedisUrl) if err != nil { return nil, fmt.Errorf("error parsing the redis url: %w", err) } log.Println("Connecting to Redis") rdb := redis.NewClient(opt) // verify redis connection _, err = rdb.Ping(ctx).Result() if err != nil { return nil, fmt.Errorf("error connecting to redis: %w", err) } // Initialize S3 Session sess, err := session.NewSession( &aws.Config{ Credentials: credentials.NewStaticCredentials( cfg.AccessKey, cfg.SessionSecret, "", ), Region: aws.String(cfg.Region), }, ) if err != nil { return nil, fmt.Errorf("error creating s3 session: %w", err) } return &dataSources{ DB: db, RedisClient: rdb, S3Session: sess, }, nil } // close to be used in graceful server shutdown func (d *dataSources) close() error { if err := d.RedisClient.Close(); err != nil { return fmt.Errorf("error closing Redis Client: %w", err) } return nil } ================================================ FILE: server/docs/docs.go ================================================ // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // This file was generated by swaggo/swag package docs import ( "bytes" "encoding/json" "strings" "github.com/alecthomas/template" "github.com/swaggo/swag" ) var doc = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { "description": "{{.Description}}", "title": "{{.Title}}", "contact": {}, "license": { "name": "Apache 2.0" }, "version": "{{.Version}}" }, "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { "/account": { "get": { "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Get Current User", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/User" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "put": { "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Update Current User", "parameters": [ { "description": "Update Account", "name": "account", "in": "body", "required": true, "schema": { "$ref": "#/definitions/EditUser" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/User" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/change-password": { "put": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Change Current User's Password", "parameters": [ { "description": "Change Password", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ChangePasswordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/forgot-password": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Forgot Password Request", "parameters": [ { "description": "Forgot Password", "name": "email", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ForgotPasswordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/login": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "User Login", "parameters": [ { "description": "Login account", "name": "account", "in": "body", "required": true, "schema": { "$ref": "#/definitions/LoginRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/User" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/logout": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "User Logout", "parameters": [ { "description": "Login account", "name": "account", "in": "body", "required": true, "schema": { "$ref": "#/definitions/LoginRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } } } } }, "/account/me/friends": { "get": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Get Current User's Friends", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Friend" } } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/me/pending": { "get": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Get Current User's Friend Requests", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/FriendRequest" } } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/register": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Create an Account", "parameters": [ { "description": "Create account", "name": "account", "in": "body", "required": true, "schema": { "$ref": "#/definitions/RegisterRequest" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/User" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/reset-password": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Reset Password", "parameters": [ { "description": "Reset Password", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ResetPasswordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/User" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/{memberId}/friend": { "post": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Send Friend Request", "parameters": [ { "type": "string", "description": "User ID", "name": "memberId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Remove Friend", "parameters": [ { "type": "string", "description": "User ID", "name": "memberId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/{memberId}/friend/accept": { "post": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Accept Friend's Request", "parameters": [ { "type": "string", "description": "User ID", "name": "memberId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/{memberId}/friend/cancel": { "post": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Cancel Friend's Request", "parameters": [ { "type": "string", "description": "User ID", "name": "memberId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/me/dm": { "get": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Get User's DMs", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/DirectMessage" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{channelId}": { "put": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Edit Channel", "parameters": [ { "type": "string", "description": "Channel ID", "name": "channelId", "in": "path", "required": true }, { "description": "Edit Channel", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ChannelRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{channelId}/dm": { "post": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Get or Create DM", "parameters": [ { "type": "string", "description": "Member ID", "name": "channelId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/DirectMessage" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{channelId}/members": { "get": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Get Members of the given Channel", "parameters": [ { "type": "string", "description": "Channel ID", "name": "channelId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{guildId}": { "get": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Get Guild Channels", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Channel" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "post": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Create Channel", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Channel" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{id}": { "delete": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Delete Channel", "parameters": [ { "type": "string", "description": "Channel ID", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{id}/dm": { "delete": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Close DM", "parameters": [ { "type": "string", "description": "DM Channel ID", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds": { "get": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Get Current User's Guilds", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/GuildResponse" } } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/create": { "post": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Create Guild", "parameters": [ { "description": "Create Guild", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/CreateGuildRequest" } } ], "responses": { "201": { "description": "Created", "schema": { "type": "array", "items": { "$ref": "#/definitions/GuildResponse" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/join": { "post": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Join Guild", "parameters": [ { "description": "Join Guild", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/JoinRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/GuildResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}": { "put": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Edit Guild", "parameters": [ { "description": "Edit Guild", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/EditGuildRequest" } }, { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Leave Guild", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/bans": { "get": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Get Guild Ban list", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/BanResponse" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "post": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Ban Member", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true }, { "description": "Member ID", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MemberRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/SuccessResponse" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Unban Member", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true }, { "description": "Member ID", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MemberRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/SuccessResponse" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/delete": { "delete": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Delete Guild", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/invite": { "get": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Get Guild Invite", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true }, { "type": "boolean", "description": "Is Permanent", "name": "isPermanent", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Delete all permanent invite links", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/kick": { "post": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Kick Member", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true }, { "description": "Member ID", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MemberRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/SuccessResponse" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/member": { "get": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Get Member Settings", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/MemberSettings" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "put": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Edit Member Settings", "parameters": [ { "description": "Edit Member", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MemberSettingsRequest" } }, { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/members": { "get": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Get Guild Members", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Member" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/messages/{channelId}": { "get": { "produces": [ "application/json" ], "tags": [ "Messages" ], "summary": "Get Channel Messages", "parameters": [ { "type": "string", "description": "Channel ID", "name": "channelId", "in": "path", "required": true }, { "type": "string", "description": "Cursor Pagination using the createdAt field", "name": "cursor", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Message" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "post": { "produces": [ "application/json" ], "tags": [ "Messages" ], "summary": "Create Messages", "parameters": [ { "type": "string", "description": "Channel ID", "name": "channelId", "in": "path", "required": true }, { "description": "Create Message", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MessageRequest" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/messages/{messageId}": { "put": { "produces": [ "application/json" ], "tags": [ "Messages" ], "summary": "Edit Messages", "parameters": [ { "type": "string", "description": "Message ID", "name": "messageId", "in": "path", "required": true }, { "description": "Edit Message", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MessageRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Messages" ], "summary": "Delete Messages", "parameters": [ { "type": "string", "description": "Message ID", "name": "messageId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } } }, "definitions": { "Attachment": { "type": "object", "properties": { "filename": { "type": "string" }, "filetype": { "type": "string" }, "url": { "type": "string" } } }, "BanResponse": { "type": "object", "properties": { "id": { "type": "string" }, "image": { "type": "string" }, "username": { "type": "string" } } }, "ChangePasswordRequest": { "type": "object", "properties": { "confirmNewPassword": { "description": "Must be the same as the newPassword value.", "type": "string" }, "currentPassword": { "type": "string" }, "newPassword": { "description": "Min 6, max 150 characters.", "type": "string" } } }, "Channel": { "type": "object", "properties": { "createdAt": { "type": "string" }, "hasNotification": { "type": "boolean" }, "id": { "type": "string" }, "isPublic": { "type": "boolean" }, "name": { "type": "string" }, "updatedAt": { "type": "string" } } }, "ChannelRequest": { "type": "object", "properties": { "isPublic": { "description": "Default is true", "type": "boolean" }, "members": { "description": "Array of memberIds", "type": "array", "items": { "type": "string" } }, "name": { "description": "Channel Name. 3 to 30 character", "type": "string" } } }, "CreateGuildRequest": { "type": "object", "properties": { "name": { "description": "Guild Name. 3 to 30 characters", "type": "string" } } }, "DMUser": { "type": "object", "properties": { "id": { "type": "string" }, "image": { "type": "string" }, "isFriend": { "type": "boolean" }, "isOnline": { "type": "boolean" }, "username": { "type": "string" } } }, "DirectMessage": { "type": "object", "properties": { "id": { "type": "string" }, "user": { "$ref": "#/definitions/DMUser" } } }, "EditGuildRequest": { "type": "object", "properties": { "icon": { "description": "The old guild icon url if no new image is selected. Set to null to reset the guild icon", "type": "string" }, "image": { "description": "image/png or image/jpeg", "type": "string", "format": "binary" }, "name": { "description": "Guild Name. 3 to 30 characters", "type": "string" } } }, "EditUser": { "type": "object", "properties": { "email": { "description": "Must be unique", "type": "string" }, "image": { "description": "image/png or image/jpeg", "type": "string", "format": "binary" }, "username": { "description": "Min 3, max 30 characters.", "type": "string" } } }, "ErrorResponse": { "type": "object", "properties": { "error": { "$ref": "#/definitions/HttpError" } } }, "ErrorsResponse": { "type": "object", "properties": { "errors": { "type": "array", "items": { "$ref": "#/definitions/FieldError" } } } }, "FieldError": { "type": "object", "properties": { "field": { "description": "The property containing the error", "type": "string" }, "message": { "description": "The specific error message", "type": "string" } } }, "ForgotPasswordRequest": { "type": "object", "properties": { "email": { "type": "string" } } }, "Friend": { "type": "object", "properties": { "id": { "type": "string" }, "image": { "type": "string" }, "isOnline": { "type": "boolean" }, "username": { "type": "string" } } }, "FriendRequest": { "type": "object", "properties": { "id": { "type": "string" }, "image": { "type": "string" }, "type": { "description": "1: Incoming, 0: Outgoing", "type": "integer", "enum": [ 0, 1 ] }, "username": { "type": "string" } } }, "GuildResponse": { "type": "object", "properties": { "createdAt": { "type": "string" }, "default_channel_id": { "type": "string" }, "hasNotification": { "type": "boolean" }, "icon": { "type": "string" }, "id": { "type": "string" }, "name": { "type": "string" }, "ownerId": { "type": "string" }, "updatedAt": { "type": "string" } } }, "HttpError": { "type": "object", "properties": { "message": { "description": "The specific error message", "type": "string" }, "type": { "description": "The Http Response as a string", "type": "string" } } }, "JoinRequest": { "type": "object", "properties": { "link": { "type": "string" } } }, "LoginRequest": { "type": "object", "properties": { "email": { "description": "Must be unique", "type": "string" }, "password": { "description": "Min 6, max 150 characters.", "type": "string" } } }, "Member": { "type": "object", "properties": { "color": { "type": "string" }, "createdAt": { "type": "string" }, "id": { "type": "string" }, "image": { "type": "string" }, "isFriend": { "type": "boolean" }, "isOnline": { "type": "boolean" }, "nickname": { "type": "string" }, "updatedAt": { "type": "string" }, "username": { "type": "string" } } }, "MemberRequest": { "type": "object", "properties": { "memberId": { "type": "string" } } }, "MemberSettings": { "type": "object", "properties": { "color": { "type": "string" }, "nickname": { "type": "string" } } }, "MemberSettingsRequest": { "type": "object", "properties": { "color": { "type": "string" }, "nickname": { "type": "string" } } }, "Message": { "type": "object", "properties": { "attachment": { "$ref": "#/definitions/Attachment" }, "createdAt": { "type": "string" }, "id": { "type": "string" }, "text": { "type": "string" }, "updatedAt": { "type": "string" }, "user": { "$ref": "#/definitions/Member" } } }, "MessageRequest": { "type": "object", "properties": { "file": { "description": "image/* or audio/*", "type": "string", "format": "binary" }, "text": { "description": "Maximum 2000 characters", "type": "string" } } }, "RegisterRequest": { "type": "object", "properties": { "email": { "description": "Must be unique", "type": "string" }, "password": { "description": "Min 6, max 150 characters.", "type": "string" }, "username": { "description": "Min 3, max 30 characters.", "type": "string" } } }, "ResetPasswordRequest": { "type": "object", "properties": { "confirmNewPassword": { "description": "Must be the same as the password value.", "type": "string" }, "newPassword": { "description": "Min 6, max 150 characters.", "type": "string" }, "token": { "description": "The token the user got from the email.", "type": "string" } } }, "SuccessResponse": { "type": "object", "properties": { "success": { "description": "Only returns true, not a json object", "type": "boolean" } } }, "User": { "type": "object", "properties": { "createdAt": { "type": "string" }, "email": { "type": "string" }, "id": { "type": "string" }, "image": { "type": "string" }, "isOnline": { "type": "boolean" }, "updatedAt": { "type": "string" }, "username": { "type": "string" } } } } }` type swaggerInfo struct { Version string Host string BasePath string Schemes []string Title string Description string } // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = swaggerInfo{ Version: "1.0", Host: "localhost:", BasePath: "/api", Schemes: []string{}, Title: "Valkyrie API", Description: "Valkyrie REST API Specs. This service uses sessions for authentication", } type s struct{} func (s *s) ReadDoc() string { sInfo := SwaggerInfo sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) t, err := template.New("swagger_info").Funcs(template.FuncMap{ "marshal": func(v any) string { a, _ := json.Marshal(v) return string(a) }, }).Parse(doc) if err != nil { return doc } var tpl bytes.Buffer if err := t.Execute(&tpl, sInfo); err != nil { return doc } return tpl.String() } func init() { swag.Register(swag.Name, &s{}) } ================================================ FILE: server/docs/swagger.json ================================================ { "swagger": "2.0", "info": { "description": "Valkyrie REST API Specs. This service uses sessions for authentication", "title": "Valkyrie API", "contact": {}, "license": { "name": "Apache 2.0" }, "version": "1.0" }, "host": "localhost:\u003cPORT\u003e", "basePath": "/api", "paths": { "/account": { "get": { "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Get Current User", "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/User" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "put": { "consumes": [ "multipart/form-data" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Update Current User", "parameters": [ { "description": "Update Account", "name": "account", "in": "body", "required": true, "schema": { "$ref": "#/definitions/EditUser" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/User" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/change-password": { "put": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Change Current User's Password", "parameters": [ { "description": "Change Password", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ChangePasswordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/forgot-password": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Forgot Password Request", "parameters": [ { "description": "Forgot Password", "name": "email", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ForgotPasswordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/login": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "User Login", "parameters": [ { "description": "Login account", "name": "account", "in": "body", "required": true, "schema": { "$ref": "#/definitions/LoginRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/User" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/logout": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "User Logout", "parameters": [ { "description": "Login account", "name": "account", "in": "body", "required": true, "schema": { "$ref": "#/definitions/LoginRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } } } } }, "/account/me/friends": { "get": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Get Current User's Friends", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Friend" } } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/me/pending": { "get": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Get Current User's Friend Requests", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/FriendRequest" } } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/register": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Create an Account", "parameters": [ { "description": "Create account", "name": "account", "in": "body", "required": true, "schema": { "$ref": "#/definitions/RegisterRequest" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/User" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/reset-password": { "post": { "consumes": [ "application/json" ], "produces": [ "application/json" ], "tags": [ "Account" ], "summary": "Reset Password", "parameters": [ { "description": "Reset Password", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ResetPasswordRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/User" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/{memberId}/friend": { "post": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Send Friend Request", "parameters": [ { "type": "string", "description": "User ID", "name": "memberId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Remove Friend", "parameters": [ { "type": "string", "description": "User ID", "name": "memberId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/{memberId}/friend/accept": { "post": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Accept Friend's Request", "parameters": [ { "type": "string", "description": "User ID", "name": "memberId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/account/{memberId}/friend/cancel": { "post": { "produces": [ "application/json" ], "tags": [ "Friends" ], "summary": "Cancel Friend's Request", "parameters": [ { "type": "string", "description": "User ID", "name": "memberId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/me/dm": { "get": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Get User's DMs", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/DirectMessage" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{channelId}": { "put": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Edit Channel", "parameters": [ { "type": "string", "description": "Channel ID", "name": "channelId", "in": "path", "required": true }, { "description": "Edit Channel", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/ChannelRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{channelId}/dm": { "post": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Get or Create DM", "parameters": [ { "type": "string", "description": "Member ID", "name": "channelId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/DirectMessage" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{channelId}/members": { "get": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Get Members of the given Channel", "parameters": [ { "type": "string", "description": "Channel ID", "name": "channelId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "type": "string" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{guildId}": { "get": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Get Guild Channels", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Channel" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "post": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Create Channel", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Channel" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{id}": { "delete": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Delete Channel", "parameters": [ { "type": "string", "description": "Channel ID", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/channels/{id}/dm": { "delete": { "produces": [ "application/json" ], "tags": [ "Channels" ], "summary": "Close DM", "parameters": [ { "type": "string", "description": "DM Channel ID", "name": "id", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds": { "get": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Get Current User's Guilds", "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/GuildResponse" } } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/create": { "post": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Create Guild", "parameters": [ { "description": "Create Guild", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/CreateGuildRequest" } } ], "responses": { "201": { "description": "Created", "schema": { "type": "array", "items": { "$ref": "#/definitions/GuildResponse" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/join": { "post": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Join Guild", "parameters": [ { "description": "Join Guild", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/JoinRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/GuildResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}": { "put": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Edit Guild", "parameters": [ { "description": "Edit Guild", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/EditGuildRequest" } }, { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Leave Guild", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/bans": { "get": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Get Guild Ban list", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/BanResponse" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "post": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Ban Member", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true }, { "description": "Member ID", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MemberRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/SuccessResponse" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Unban Member", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true }, { "description": "Member ID", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MemberRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/SuccessResponse" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/delete": { "delete": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Delete Guild", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/invite": { "get": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Get Guild Invite", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true }, { "type": "boolean", "description": "Is Permanent", "name": "isPermanent", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "string" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Delete all permanent invite links", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/kick": { "post": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Kick Member", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true }, { "description": "Member ID", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MemberRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/SuccessResponse" } } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/member": { "get": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Get Member Settings", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/MemberSettings" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "put": { "produces": [ "application/json" ], "tags": [ "Members" ], "summary": "Edit Member Settings", "parameters": [ { "description": "Edit Member", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MemberSettingsRequest" } }, { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/guilds/{guildId}/members": { "get": { "produces": [ "application/json" ], "tags": [ "Guilds" ], "summary": "Get Guild Members", "parameters": [ { "type": "string", "description": "Guild ID", "name": "guildId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Member" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/messages/{channelId}": { "get": { "produces": [ "application/json" ], "tags": [ "Messages" ], "summary": "Get Channel Messages", "parameters": [ { "type": "string", "description": "Channel ID", "name": "channelId", "in": "path", "required": true }, { "type": "string", "description": "Cursor Pagination using the createdAt field", "name": "cursor", "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { "$ref": "#/definitions/Message" } } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "post": { "produces": [ "application/json" ], "tags": [ "Messages" ], "summary": "Create Messages", "parameters": [ { "type": "string", "description": "Channel ID", "name": "channelId", "in": "path", "required": true }, { "description": "Create Message", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MessageRequest" } } ], "responses": { "201": { "description": "Created", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } }, "/messages/{messageId}": { "put": { "produces": [ "application/json" ], "tags": [ "Messages" ], "summary": "Edit Messages", "parameters": [ { "type": "string", "description": "Message ID", "name": "messageId", "in": "path", "required": true }, { "description": "Edit Message", "name": "request", "in": "body", "required": true, "schema": { "$ref": "#/definitions/MessageRequest" } } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "400": { "description": "Bad Request", "schema": { "$ref": "#/definitions/ErrorsResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } }, "delete": { "produces": [ "application/json" ], "tags": [ "Messages" ], "summary": "Delete Messages", "parameters": [ { "type": "string", "description": "Message ID", "name": "messageId", "in": "path", "required": true } ], "responses": { "200": { "description": "OK", "schema": { "$ref": "#/definitions/SuccessResponse" } }, "401": { "description": "Unauthorized", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "404": { "description": "Not Found", "schema": { "$ref": "#/definitions/ErrorResponse" } }, "500": { "description": "Internal Server Error", "schema": { "$ref": "#/definitions/ErrorResponse" } } } } } }, "definitions": { "Attachment": { "type": "object", "properties": { "filename": { "type": "string" }, "filetype": { "type": "string" }, "url": { "type": "string" } } }, "BanResponse": { "type": "object", "properties": { "id": { "type": "string" }, "image": { "type": "string" }, "username": { "type": "string" } } }, "ChangePasswordRequest": { "type": "object", "properties": { "confirmNewPassword": { "description": "Must be the same as the newPassword value.", "type": "string" }, "currentPassword": { "type": "string" }, "newPassword": { "description": "Min 6, max 150 characters.", "type": "string" } } }, "Channel": { "type": "object", "properties": { "createdAt": { "type": "string" }, "hasNotification": { "type": "boolean" }, "id": { "type": "string" }, "isPublic": { "type": "boolean" }, "name": { "type": "string" }, "updatedAt": { "type": "string" } } }, "ChannelRequest": { "type": "object", "properties": { "isPublic": { "description": "Default is true", "type": "boolean" }, "members": { "description": "Array of memberIds", "type": "array", "items": { "type": "string" } }, "name": { "description": "Channel Name. 3 to 30 character", "type": "string" } } }, "CreateGuildRequest": { "type": "object", "properties": { "name": { "description": "Guild Name. 3 to 30 characters", "type": "string" } } }, "DMUser": { "type": "object", "properties": { "id": { "type": "string" }, "image": { "type": "string" }, "isFriend": { "type": "boolean" }, "isOnline": { "type": "boolean" }, "username": { "type": "string" } } }, "DirectMessage": { "type": "object", "properties": { "id": { "type": "string" }, "user": { "$ref": "#/definitions/DMUser" } } }, "EditGuildRequest": { "type": "object", "properties": { "icon": { "description": "The old guild icon url if no new image is selected. Set to null to reset the guild icon", "type": "string" }, "image": { "description": "image/png or image/jpeg", "type": "string", "format": "binary" }, "name": { "description": "Guild Name. 3 to 30 characters", "type": "string" } } }, "EditUser": { "type": "object", "properties": { "email": { "description": "Must be unique", "type": "string" }, "image": { "description": "image/png or image/jpeg", "type": "string", "format": "binary" }, "username": { "description": "Min 3, max 30 characters.", "type": "string" } } }, "ErrorResponse": { "type": "object", "properties": { "error": { "$ref": "#/definitions/HttpError" } } }, "ErrorsResponse": { "type": "object", "properties": { "errors": { "type": "array", "items": { "$ref": "#/definitions/FieldError" } } } }, "FieldError": { "type": "object", "properties": { "field": { "description": "The property containing the error", "type": "string" }, "message": { "description": "The specific error message", "type": "string" } } }, "ForgotPasswordRequest": { "type": "object", "properties": { "email": { "type": "string" } } }, "Friend": { "type": "object", "properties": { "id": { "type": "string" }, "image": { "type": "string" }, "isOnline": { "type": "boolean" }, "username": { "type": "string" } } }, "FriendRequest": { "type": "object", "properties": { "id": { "type": "string" }, "image": { "type": "string" }, "type": { "description": "1: Incoming, 0: Outgoing", "type": "integer", "enum": [ 0, 1 ] }, "username": { "type": "string" } } }, "GuildResponse": { "type": "object", "properties": { "createdAt": { "type": "string" }, "default_channel_id": { "type": "string" }, "hasNotification": { "type": "boolean" }, "icon": { "type": "string" }, "id": { "type": "string" }, "name": { "type": "string" }, "ownerId": { "type": "string" }, "updatedAt": { "type": "string" } } }, "HttpError": { "type": "object", "properties": { "message": { "description": "The specific error message", "type": "string" }, "type": { "description": "The Http Response as a string", "type": "string" } } }, "JoinRequest": { "type": "object", "properties": { "link": { "type": "string" } } }, "LoginRequest": { "type": "object", "properties": { "email": { "description": "Must be unique", "type": "string" }, "password": { "description": "Min 6, max 150 characters.", "type": "string" } } }, "Member": { "type": "object", "properties": { "color": { "type": "string" }, "createdAt": { "type": "string" }, "id": { "type": "string" }, "image": { "type": "string" }, "isFriend": { "type": "boolean" }, "isOnline": { "type": "boolean" }, "nickname": { "type": "string" }, "updatedAt": { "type": "string" }, "username": { "type": "string" } } }, "MemberRequest": { "type": "object", "properties": { "memberId": { "type": "string" } } }, "MemberSettings": { "type": "object", "properties": { "color": { "type": "string" }, "nickname": { "type": "string" } } }, "MemberSettingsRequest": { "type": "object", "properties": { "color": { "type": "string" }, "nickname": { "type": "string" } } }, "Message": { "type": "object", "properties": { "attachment": { "$ref": "#/definitions/Attachment" }, "createdAt": { "type": "string" }, "id": { "type": "string" }, "text": { "type": "string" }, "updatedAt": { "type": "string" }, "user": { "$ref": "#/definitions/Member" } } }, "MessageRequest": { "type": "object", "properties": { "file": { "description": "image/* or audio/*", "type": "string", "format": "binary" }, "text": { "description": "Maximum 2000 characters", "type": "string" } } }, "RegisterRequest": { "type": "object", "properties": { "email": { "description": "Must be unique", "type": "string" }, "password": { "description": "Min 6, max 150 characters.", "type": "string" }, "username": { "description": "Min 3, max 30 characters.", "type": "string" } } }, "ResetPasswordRequest": { "type": "object", "properties": { "confirmNewPassword": { "description": "Must be the same as the password value.", "type": "string" }, "newPassword": { "description": "Min 6, max 150 characters.", "type": "string" }, "token": { "description": "The token the user got from the email.", "type": "string" } } }, "SuccessResponse": { "type": "object", "properties": { "success": { "description": "Only returns true, not a json object", "type": "boolean" } } }, "User": { "type": "object", "properties": { "createdAt": { "type": "string" }, "email": { "type": "string" }, "id": { "type": "string" }, "image": { "type": "string" }, "isOnline": { "type": "boolean" }, "updatedAt": { "type": "string" }, "username": { "type": "string" } } } } } ================================================ FILE: server/docs/swagger.yaml ================================================ basePath: /api definitions: Attachment: properties: filename: type: string filetype: type: string url: type: string type: object BanResponse: properties: id: type: string image: type: string username: type: string type: object ChangePasswordRequest: properties: confirmNewPassword: description: Must be the same as the newPassword value. type: string currentPassword: type: string newPassword: description: Min 6, max 150 characters. type: string type: object Channel: properties: createdAt: type: string hasNotification: type: boolean id: type: string isPublic: type: boolean name: type: string updatedAt: type: string type: object ChannelRequest: properties: isPublic: description: Default is true type: boolean members: description: Array of memberIds items: type: string type: array name: description: Channel Name. 3 to 30 character type: string type: object CreateGuildRequest: properties: name: description: Guild Name. 3 to 30 characters type: string type: object DMUser: properties: id: type: string image: type: string isFriend: type: boolean isOnline: type: boolean username: type: string type: object DirectMessage: properties: id: type: string user: $ref: '#/definitions/DMUser' type: object EditGuildRequest: properties: icon: description: The old guild icon url if no new image is selected. Set to null to reset the guild icon type: string image: description: image/png or image/jpeg format: binary type: string name: description: Guild Name. 3 to 30 characters type: string type: object EditUser: properties: email: description: Must be unique type: string image: description: image/png or image/jpeg format: binary type: string username: description: Min 3, max 30 characters. type: string type: object ErrorResponse: properties: error: $ref: '#/definitions/HttpError' type: object ErrorsResponse: properties: errors: items: $ref: '#/definitions/FieldError' type: array type: object FieldError: properties: field: description: The property containing the error type: string message: description: The specific error message type: string type: object ForgotPasswordRequest: properties: email: type: string type: object Friend: properties: id: type: string image: type: string isOnline: type: boolean username: type: string type: object FriendRequest: properties: id: type: string image: type: string type: description: '1: Incoming, 0: Outgoing' enum: - 0 - 1 type: integer username: type: string type: object GuildResponse: properties: createdAt: type: string default_channel_id: type: string hasNotification: type: boolean icon: type: string id: type: string name: type: string ownerId: type: string updatedAt: type: string type: object HttpError: properties: message: description: The specific error message type: string type: description: The Http Response as a string type: string type: object JoinRequest: properties: link: type: string type: object LoginRequest: properties: email: description: Must be unique type: string password: description: Min 6, max 150 characters. type: string type: object Member: properties: color: type: string createdAt: type: string id: type: string image: type: string isFriend: type: boolean isOnline: type: boolean nickname: type: string updatedAt: type: string username: type: string type: object MemberRequest: properties: memberId: type: string type: object MemberSettings: properties: color: type: string nickname: type: string type: object MemberSettingsRequest: properties: color: type: string nickname: type: string type: object Message: properties: attachment: $ref: '#/definitions/Attachment' createdAt: type: string id: type: string text: type: string updatedAt: type: string user: $ref: '#/definitions/Member' type: object MessageRequest: properties: file: description: image/* or audio/* format: binary type: string text: description: Maximum 2000 characters type: string type: object RegisterRequest: properties: email: description: Must be unique type: string password: description: Min 6, max 150 characters. type: string username: description: Min 3, max 30 characters. type: string type: object ResetPasswordRequest: properties: confirmNewPassword: description: Must be the same as the password value. type: string newPassword: description: Min 6, max 150 characters. type: string token: description: The token the user got from the email. type: string type: object SuccessResponse: properties: success: description: Only returns true, not a json object type: boolean type: object User: properties: createdAt: type: string email: type: string id: type: string image: type: string isOnline: type: boolean updatedAt: type: string username: type: string type: object host: localhost: info: contact: {} description: Valkyrie REST API Specs. This service uses sessions for authentication license: name: Apache 2.0 title: Valkyrie API version: "1.0" paths: /account: get: produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/User' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Current User tags: - Account put: consumes: - multipart/form-data parameters: - description: Update Account in: body name: account required: true schema: $ref: '#/definitions/EditUser' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/User' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Update Current User tags: - Account /account/{memberId}/friend: delete: parameters: - description: User ID in: path name: memberId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Remove Friend tags: - Friends post: parameters: - description: User ID in: path name: memberId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Send Friend Request tags: - Friends /account/{memberId}/friend/accept: post: parameters: - description: User ID in: path name: memberId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Accept Friend's Request tags: - Friends /account/{memberId}/friend/cancel: post: parameters: - description: User ID in: path name: memberId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Cancel Friend's Request tags: - Friends /account/change-password: put: consumes: - application/json parameters: - description: Change Password in: body name: request required: true schema: $ref: '#/definitions/ChangePasswordRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Change Current User's Password tags: - Account /account/forgot-password: post: consumes: - application/json parameters: - description: Forgot Password in: body name: email required: true schema: $ref: '#/definitions/ForgotPasswordRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Forgot Password Request tags: - Account /account/login: post: consumes: - application/json parameters: - description: Login account in: body name: account required: true schema: $ref: '#/definitions/LoginRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/User' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: User Login tags: - Account /account/logout: post: consumes: - application/json parameters: - description: Login account in: body name: account required: true schema: $ref: '#/definitions/LoginRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' summary: User Logout tags: - Account /account/me/friends: get: produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/Friend' type: array "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Current User's Friends tags: - Friends /account/me/pending: get: produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/FriendRequest' type: array "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Current User's Friend Requests tags: - Friends /account/register: post: consumes: - application/json parameters: - description: Create account in: body name: account required: true schema: $ref: '#/definitions/RegisterRequest' produces: - application/json responses: "201": description: Created schema: $ref: '#/definitions/User' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Create an Account tags: - Account /account/reset-password: post: consumes: - application/json parameters: - description: Reset Password in: body name: request required: true schema: $ref: '#/definitions/ResetPasswordRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/User' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Reset Password tags: - Account /channels/{channelId}: put: parameters: - description: Channel ID in: path name: channelId required: true type: string - description: Edit Channel in: body name: request required: true schema: $ref: '#/definitions/ChannelRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Edit Channel tags: - Channels /channels/{channelId}/dm: post: parameters: - description: Member ID in: path name: channelId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/DirectMessage' "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Get or Create DM tags: - Channels /channels/{channelId}/members: get: parameters: - description: Channel ID in: path name: channelId required: true type: string produces: - application/json responses: "200": description: OK schema: items: type: string type: array "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Members of the given Channel tags: - Channels /channels/{guildId}: get: parameters: - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/Channel' type: array "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Guild Channels tags: - Channels post: parameters: - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/Channel' type: array "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Create Channel tags: - Channels /channels/{id}: delete: parameters: - description: Channel ID in: path name: id required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Delete Channel tags: - Channels /channels/{id}/dm: delete: parameters: - description: DM Channel ID in: path name: id required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Close DM tags: - Channels /channels/me/dm: get: produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/DirectMessage' type: array "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get User's DMs tags: - Channels /guilds: get: produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/GuildResponse' type: array "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Current User's Guilds tags: - Guilds /guilds/{guildId}: delete: parameters: - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Leave Guild tags: - Guilds put: parameters: - description: Edit Guild in: body name: request required: true schema: $ref: '#/definitions/EditGuildRequest' - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Edit Guild tags: - Guilds /guilds/{guildId}/bans: delete: parameters: - description: Guild ID in: path name: guildId required: true type: string - description: Member ID in: body name: request required: true schema: $ref: '#/definitions/MemberRequest' produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/SuccessResponse' type: array "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Unban Member tags: - Members get: parameters: - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/BanResponse' type: array "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Get Guild Ban list tags: - Members post: parameters: - description: Guild ID in: path name: guildId required: true type: string - description: Member ID in: body name: request required: true schema: $ref: '#/definitions/MemberRequest' produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/SuccessResponse' type: array "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Ban Member tags: - Members /guilds/{guildId}/delete: delete: parameters: - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Delete Guild tags: - Guilds /guilds/{guildId}/invite: delete: parameters: - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Delete all permanent invite links tags: - Guilds get: parameters: - description: Guild ID in: path name: guildId required: true type: string - description: Is Permanent in: query name: isPermanent type: boolean produces: - application/json responses: "200": description: OK schema: type: string "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Get Guild Invite tags: - Guilds /guilds/{guildId}/kick: post: parameters: - description: Guild ID in: path name: guildId required: true type: string - description: Member ID in: body name: request required: true schema: $ref: '#/definitions/MemberRequest' produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/SuccessResponse' type: array "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Kick Member tags: - Members /guilds/{guildId}/member: get: parameters: - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/MemberSettings' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Member Settings tags: - Members put: parameters: - description: Edit Member in: body name: request required: true schema: $ref: '#/definitions/MemberSettingsRequest' - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Edit Member Settings tags: - Members /guilds/{guildId}/members: get: parameters: - description: Guild ID in: path name: guildId required: true type: string produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/Member' type: array "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Guild Members tags: - Guilds /guilds/create: post: parameters: - description: Create Guild in: body name: request required: true schema: $ref: '#/definitions/CreateGuildRequest' produces: - application/json responses: "201": description: Created schema: items: $ref: '#/definitions/GuildResponse' type: array "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Create Guild tags: - Guilds /guilds/join: post: parameters: - description: Join Guild in: body name: request required: true schema: $ref: '#/definitions/JoinRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/GuildResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Join Guild tags: - Guilds /messages/{channelId}: get: parameters: - description: Channel ID in: path name: channelId required: true type: string - description: Cursor Pagination using the createdAt field in: query name: cursor type: string produces: - application/json responses: "200": description: OK schema: items: $ref: '#/definitions/Message' type: array "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' summary: Get Channel Messages tags: - Messages post: parameters: - description: Channel ID in: path name: channelId required: true type: string - description: Create Message in: body name: request required: true schema: $ref: '#/definitions/MessageRequest' produces: - application/json responses: "201": description: Created schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Create Messages tags: - Messages /messages/{messageId}: delete: parameters: - description: Message ID in: path name: messageId required: true type: string produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Delete Messages tags: - Messages put: parameters: - description: Message ID in: path name: messageId required: true type: string - description: Edit Message in: body name: request required: true schema: $ref: '#/definitions/MessageRequest' produces: - application/json responses: "200": description: OK schema: $ref: '#/definitions/SuccessResponse' "400": description: Bad Request schema: $ref: '#/definitions/ErrorsResponse' "401": description: Unauthorized schema: $ref: '#/definitions/ErrorResponse' "404": description: Not Found schema: $ref: '#/definitions/ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/ErrorResponse' summary: Edit Messages tags: - Messages swagger: "2.0" ================================================ FILE: server/e2e_test.go ================================================ package main import ( "bytes" "context" "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/joho/godotenv" "github.com/sentrionic/valkyrie/config" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func setupTest(t *testing.T) *gin.Engine { gin.SetMode(gin.ReleaseMode) ctx := context.Background() // Load config for local testing, ignore for CI _ = godotenv.Load() cfg, err := config.LoadConfig(ctx) assert.NoError(t, err) ds, err := initDS(ctx, cfg) assert.NoError(t, err) router, err := inject(ds, cfg) assert.NoError(t, err) return router } func TestMain_AccountE2E(t *testing.T) { router := setupTest(t) authUser := fixture.GetMockUser() cookie := "" mockUsername := fixture.Username() newPassword := fixture.RandStr(10) testCases := []struct { name string setupRequest func() (*http.Request, error) setupHeaders func(t *testing.T, request *http.Request) checkResponse func(recorder *httptest.ResponseRecorder) }{ { name: "Register Account", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": authUser.Username, "password": authUser.Password, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.Contains(t, recorder.Header(), "Set-Cookie") authUser.ID = respBody.ID }, }, { name: "Login Account", setupRequest: func() (*http.Request, error) { data := gin.H{ "password": authUser.Password, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/login", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.Equal(t, authUser.ID, respBody.ID) assert.NotNil(t, respBody.Image) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.Contains(t, recorder.Header(), "Set-Cookie") cookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Get Account", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/account", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.Equal(t, authUser.ID, respBody.ID) assert.NotNil(t, respBody.Image) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) }, }, { name: "Edit Account", setupRequest: func() (*http.Request, error) { form := url.Values{} form.Add("username", mockUsername) form.Add("email", authUser.Email) request, err := http.NewRequest(http.MethodPut, "/api/account", strings.NewReader(form.Encode())) if err != nil { return nil, err } request.Form = form return request, nil }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockUsername, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.Equal(t, authUser.ID, respBody.ID) assert.NotNil(t, respBody.Image) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) }, }, { name: "Change Password", setupRequest: func() (*http.Request, error) { data := gin.H{ "currentPassword": authUser.Password, "newPassword": newPassword, "confirmNewPassword": newPassword, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPut, "/api/account/change-password", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Login to verify changes", setupRequest: func() (*http.Request, error) { data := gin.H{ "password": newPassword, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/login", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockUsername, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.Equal(t, authUser.ID, respBody.ID) assert.NotNil(t, respBody.Image) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) }, }, { name: "Logout", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodPost, "/api/account/logout", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) }, }, { name: "Check session is invalid now", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/account", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusUnauthorized, recorder.Code) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() request, err := tc.setupRequest() tc.setupHeaders(t, request) assert.NoError(t, err) router.ServeHTTP(rr, request) tc.checkResponse(rr) }) } } func TestMain_FriendsE2E(t *testing.T) { router := setupTest(t) authUser := fixture.GetMockUser() cookie := "" mockUser := fixture.GetMockUser() mockUserCookie := "" testCases := []struct { name string setupRequest func() (*http.Request, error) setupHeaders func(t *testing.T, request *http.Request) checkResponse func(recorder *httptest.ResponseRecorder) }{ { name: "Register Account", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": authUser.Username, "password": authUser.Password, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.True(t, respBody.IsOnline) assert.Equal(t, authUser.Image, respBody.Image) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.Contains(t, recorder.Header(), "Set-Cookie") authUser.ID = respBody.ID cookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Register Friend", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": mockUser.Username, "password": mockUser.Password, "email": mockUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockUser.Username, respBody.Username) assert.Equal(t, mockUser.Email, respBody.Email) assert.True(t, respBody.IsOnline) assert.Equal(t, mockUser.Image, respBody.Image) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) mockUser.ID = respBody.ID mockUserCookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Send friend request", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) return http.NewRequest(http.MethodPost, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Get friend requests", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/account/me/pending", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.FriendRequest{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) requests := *respBody assert.Equal(t, 1, len(requests)) request := requests[0] assert.Equal(t, authUser.Username, request.Username) assert.Equal(t, authUser.ID, request.Id) assert.Equal(t, authUser.Image, request.Image) assert.Equal(t, model.Incoming, request.Type) }, }, { name: "Accept friend request", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/account/%s/friend/accept", authUser.ID) return http.NewRequest(http.MethodPost, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Check that requests are empty now", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/account/me/pending", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.FriendRequest{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) requests := *respBody assert.Equal(t, 0, len(requests)) }, }, { name: "Get friends", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/account/me/friends", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.Friend{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) friends := *respBody assert.Equal(t, 1, len(friends)) friend := friends[0] assert.Equal(t, authUser.Username, friend.Username) assert.Equal(t, authUser.ID, friend.Id) assert.Equal(t, authUser.Image, friend.Image) assert.True(t, friend.IsOnline) }, }, { name: "Remove friend", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/account/%s/friend", authUser.ID) return http.NewRequest(http.MethodDelete, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Confirm friends list is empty", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/account/me/friends", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.Friend{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) friends := *respBody assert.Equal(t, 0, len(friends)) }, }, { name: "Send another friend request", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) return http.NewRequest(http.MethodPost, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Get authUser's friend requests", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/account/me/pending", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.FriendRequest{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) requests := *respBody assert.Equal(t, 1, len(requests)) request := requests[0] assert.Equal(t, mockUser.Username, request.Username) assert.Equal(t, mockUser.ID, request.Id) assert.Equal(t, mockUser.Image, request.Image) assert.Equal(t, model.Outgoing, request.Type) }, }, { name: "Cancel friend request", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/account/%s/friend/accept", mockUser.ID) return http.NewRequest(http.MethodPost, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() request, err := tc.setupRequest() tc.setupHeaders(t, request) assert.NoError(t, err) router.ServeHTTP(rr, request) tc.checkResponse(rr) }) } } func TestMain_GuildsE2E(t *testing.T) { router := setupTest(t) authUser := fixture.GetMockUser() cookie := "" mockUser := fixture.GetMockUser() mockUserCookie := "" mockGuild := fixture.GetMockGuild("") inviteLink := "" mockName := fixture.Username() testCases := []struct { name string setupRequest func() (*http.Request, error) setupHeaders func(t *testing.T, request *http.Request) checkResponse func(recorder *httptest.ResponseRecorder) }{ { name: "Register Account", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": authUser.Username, "password": authUser.Password, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.True(t, respBody.IsOnline) assert.Equal(t, authUser.Image, respBody.Image) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.Contains(t, recorder.Header(), "Set-Cookie") authUser.ID = respBody.ID cookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Register Member", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": mockUser.Username, "password": mockUser.Password, "email": mockUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockUser.Username, respBody.Username) assert.Equal(t, mockUser.Email, respBody.Email) assert.True(t, respBody.IsOnline) assert.Equal(t, mockUser.Image, respBody.Image) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) mockUser.ID = respBody.ID mockUserCookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Create Guild", setupRequest: func() (*http.Request, error) { data := gin.H{ "name": mockGuild.Name, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockGuild.Name, respBody.Name) assert.Equal(t, authUser.ID, respBody.OwnerId) assert.NotNil(t, respBody.Id) assert.Nil(t, respBody.Icon) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.False(t, respBody.HasNotification) assert.NotNil(t, respBody.DefaultChannelId) mockGuild.ID = respBody.Id mockGuild.OwnerId = authUser.ID }, }, { name: "Get authUser's guilds", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/guilds", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) guilds := *respBody assert.Equal(t, 1, len(guilds)) guild := guilds[0] assert.Equal(t, mockGuild.Name, guild.Name) assert.Equal(t, mockGuild.ID, guild.Id) assert.Equal(t, mockGuild.OwnerId, guild.OwnerId) assert.Nil(t, guild.Icon) assert.NotNil(t, guild.CreatedAt) assert.NotNil(t, guild.UpdatedAt) assert.False(t, guild.HasNotification) assert.NotNil(t, guild.DefaultChannelId) }, }, { name: "Edit Guild", setupRequest: func() (*http.Request, error) { form := url.Values{} form.Add("name", mockName) request, err := http.NewRequest(http.MethodPut, "/api/guilds/"+mockGuild.ID, strings.NewReader(form.Encode())) if err != nil { return nil, err } request.Form = form return request, nil }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.NotNil(t, respBody) }, }, { name: "Get guild invite", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) return http.NewRequest(http.MethodGet, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) err := json.Unmarshal(recorder.Body.Bytes(), &inviteLink) assert.NoError(t, err) assert.NotNil(t, inviteLink) }, }, { name: "Join Guild", setupRequest: func() (*http.Request, error) { data := gin.H{ "link": inviteLink, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockName, respBody.Name) assert.Equal(t, authUser.ID, respBody.OwnerId) assert.NotNil(t, respBody.Id) assert.Nil(t, respBody.Icon) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.False(t, respBody.HasNotification) assert.NotNil(t, respBody.DefaultChannelId) }, }, { name: "Check mockUser is in the guild", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/guilds", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) guilds := *respBody assert.Equal(t, 1, len(guilds)) guild := guilds[0] assert.Equal(t, mockName, guild.Name) assert.Equal(t, mockGuild.ID, guild.Id) assert.Equal(t, mockGuild.OwnerId, guild.OwnerId) assert.Nil(t, guild.Icon) assert.NotNil(t, guild.CreatedAt) assert.NotNil(t, guild.UpdatedAt) assert.False(t, guild.HasNotification) assert.NotNil(t, guild.DefaultChannelId) }, }, { name: "Leave Guild", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodDelete, "/api/guilds/"+mockGuild.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Check mockUser is in no guilds anymore", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/guilds", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) guilds := *respBody assert.Equal(t, 0, len(guilds)) }, }, { name: "Get guild members", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/guilds/%s/members", mockGuild.ID) return http.NewRequest(http.MethodGet, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.MemberResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) members := *respBody assert.Equal(t, 1, len(members)) member := members[0] assert.Equal(t, authUser.Username, member.Username) assert.Equal(t, authUser.ID, member.Id) assert.True(t, member.IsOnline) assert.NotNil(t, member.Image) assert.NotNil(t, member.CreatedAt) assert.NotNil(t, member.UpdatedAt) assert.Nil(t, member.Nickname) assert.Nil(t, member.Color) }, }, { name: "Delete Guild", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/guilds/%s/delete", mockGuild.ID) return http.NewRequest(http.MethodDelete, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Verify that there are no more guilds", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/guilds", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) guilds := *respBody assert.Len(t, guilds, 0) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() request, err := tc.setupRequest() tc.setupHeaders(t, request) assert.NoError(t, err) router.ServeHTTP(rr, request) tc.checkResponse(rr) }) } } func TestMain_MembersE2E(t *testing.T) { router := setupTest(t) authUser := fixture.GetMockUser() cookie := "" mockUser := fixture.GetMockUser() mockUserCookie := "" mockGuild := fixture.GetMockGuild("") inviteLink := "" testCases := []struct { name string setupRequest func() (*http.Request, error) setupHeaders func(t *testing.T, request *http.Request) checkResponse func(recorder *httptest.ResponseRecorder) }{ { name: "Register Account", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": authUser.Username, "password": authUser.Password, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.Contains(t, recorder.Header(), "Set-Cookie") authUser.ID = respBody.ID cookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Register Member", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": mockUser.Username, "password": mockUser.Password, "email": mockUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockUser.Username, respBody.Username) assert.Equal(t, mockUser.Email, respBody.Email) assert.Equal(t, mockUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.Image) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) mockUser.ID = respBody.ID mockUserCookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Create Guild", setupRequest: func() (*http.Request, error) { data := gin.H{ "name": mockGuild.Name, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockGuild.Name, respBody.Name) assert.Equal(t, authUser.ID, respBody.OwnerId) assert.NotNil(t, respBody.Id) assert.Nil(t, respBody.Icon) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.False(t, respBody.HasNotification) assert.NotNil(t, respBody.DefaultChannelId) mockGuild.ID = respBody.Id mockGuild.OwnerId = authUser.ID }, }, { name: "Edit authUser's member settings", setupRequest: func() (*http.Request, error) { data := gin.H{ "nickname": authUser.Username, "color": "#fff", } reqBody, err := json.Marshal(data) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) return http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.NotNil(t, respBody) }, }, { name: "Get authUser's member settings", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) return http.NewRequest(http.MethodGet, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &model.MemberSettings{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, *respBody.Nickname) assert.Equal(t, "#fff", *respBody.Color) }, }, { name: "Get guild invite", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/guilds/%s/invite?isPermanent=true", mockGuild.ID) return http.NewRequest(http.MethodGet, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) err := json.Unmarshal(recorder.Body.Bytes(), &inviteLink) assert.NoError(t, err) assert.NotNil(t, inviteLink) }, }, { name: "Join Guild", setupRequest: func() (*http.Request, error) { data := gin.H{ "link": inviteLink, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockGuild.Name, respBody.Name) assert.Equal(t, authUser.ID, respBody.OwnerId) assert.NotNil(t, respBody.Id) assert.Nil(t, respBody.Icon) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.False(t, respBody.HasNotification) assert.NotNil(t, respBody.DefaultChannelId) }, }, { name: "Check mockUser is in the guild", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/guilds", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) guilds := *respBody assert.Equal(t, 1, len(guilds)) guild := guilds[0] assert.Equal(t, mockGuild.Name, guild.Name) assert.Equal(t, mockGuild.ID, guild.Id) assert.Equal(t, mockGuild.OwnerId, guild.OwnerId) assert.Nil(t, guild.Icon) assert.NotNil(t, guild.CreatedAt) assert.NotNil(t, guild.UpdatedAt) assert.False(t, guild.HasNotification) assert.NotNil(t, guild.DefaultChannelId) }, }, { name: "Kick member", setupRequest: func() (*http.Request, error) { data := gin.H{ "memberId": mockUser.ID, } reqBody, err := json.Marshal(data) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) return http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.NotNil(t, respBody) }, }, { name: "Check mockUser is in no guilds anymore", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/guilds", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) guilds := *respBody assert.Equal(t, 0, len(guilds)) }, }, { name: "Rejoin Guild", setupRequest: func() (*http.Request, error) { data := gin.H{ "link": inviteLink, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockGuild.Name, respBody.Name) assert.Equal(t, authUser.ID, respBody.OwnerId) assert.NotNil(t, respBody.Id) assert.Nil(t, respBody.Icon) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.False(t, respBody.HasNotification) assert.NotNil(t, respBody.DefaultChannelId) }, }, { name: "Check mockUser is in the guild", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/guilds", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) guilds := *respBody assert.Equal(t, 1, len(guilds)) guild := guilds[0] assert.Equal(t, mockGuild.Name, guild.Name) assert.Equal(t, mockGuild.ID, guild.Id) assert.Equal(t, mockGuild.OwnerId, guild.OwnerId) assert.Nil(t, guild.Icon) assert.NotNil(t, guild.CreatedAt) assert.NotNil(t, guild.UpdatedAt) assert.False(t, guild.HasNotification) assert.NotNil(t, guild.DefaultChannelId) }, }, { name: "Ban member", setupRequest: func() (*http.Request, error) { data := gin.H{ "memberId": mockUser.ID, } reqBody, err := json.Marshal(data) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) return http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.NotNil(t, respBody) }, }, { name: "Check mockUser is in no guilds anymore", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/guilds", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", mockUserCookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) guilds := *respBody assert.Equal(t, 0, len(guilds)) }, }, { name: "Get guild ban list", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) return http.NewRequest(http.MethodGet, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.BanResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) bans := *respBody assert.Equal(t, 1, len(bans)) ban := bans[0] assert.Equal(t, mockUser.Username, ban.Username) assert.Equal(t, mockUser.ID, ban.Id) assert.NotNil(t, ban.Image) }, }, { name: "Unban member", setupRequest: func() (*http.Request, error) { data := gin.H{ "memberId": mockUser.ID, } reqBody, err := json.Marshal(data) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) return http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.NotNil(t, respBody) }, }, { name: "Verify ban list is empty", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) return http.NewRequest(http.MethodGet, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.BanResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) bans := *respBody assert.Equal(t, 0, len(bans)) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() request, err := tc.setupRequest() tc.setupHeaders(t, request) assert.NoError(t, err) router.ServeHTTP(rr, request) tc.checkResponse(rr) }) } } func TestMain_ChannelsE2E(t *testing.T) { router := setupTest(t) authUser := fixture.GetMockUser() cookie := "" mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel("") testCases := []struct { name string setupRequest func() (*http.Request, error) setupHeaders func(t *testing.T, request *http.Request) checkResponse func(recorder *httptest.ResponseRecorder) }{ { name: "Register Account", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": authUser.Username, "password": authUser.Password, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.Contains(t, recorder.Header(), "Set-Cookie") authUser.ID = respBody.ID cookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Create Guild", setupRequest: func() (*http.Request, error) { data := gin.H{ "name": mockGuild.Name, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockGuild.Name, respBody.Name) assert.Equal(t, authUser.ID, respBody.OwnerId) assert.NotNil(t, respBody.Id) assert.Nil(t, respBody.Icon) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.False(t, respBody.HasNotification) assert.NotNil(t, respBody.DefaultChannelId) mockGuild.ID = respBody.Id mockGuild.OwnerId = authUser.ID }, }, { name: "Create Channel", setupRequest: func() (*http.Request, error) { data := gin.H{ "name": mockChannel.Name, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/channels/"+mockGuild.ID, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.ChannelResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockChannel.Name, respBody.Name) assert.NotNil(t, respBody.Id) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.True(t, respBody.IsPublic) assert.False(t, respBody.HasNotification) mockChannel.ID = respBody.Id mockChannel.GuildID = &mockGuild.ID }, }, { name: "Get guild channels", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/channels/"+mockGuild.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.ChannelResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) channels := *respBody assert.Equal(t, 2, len(channels)) channel := channels[1] assert.Equal(t, mockChannel.Name, channel.Name) assert.Equal(t, mockChannel.ID, channel.Id) assert.NotNil(t, channel.CreatedAt) assert.NotNil(t, channel.UpdatedAt) assert.True(t, channel.IsPublic) assert.False(t, channel.HasNotification) }, }, { name: "Edit Channel", setupRequest: func() (*http.Request, error) { data := gin.H{ "name": mockChannel.Name, "isPublic": false, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPut, "/api/channels/"+mockChannel.ID, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Check that the channel is private now", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/channels/"+mockGuild.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.ChannelResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) channels := *respBody assert.Equal(t, 2, len(channels)) channel := channels[1] assert.Equal(t, mockChannel.Name, channel.Name) assert.Equal(t, mockChannel.ID, channel.Id) assert.NotNil(t, channel.CreatedAt) assert.NotNil(t, channel.UpdatedAt) assert.False(t, channel.IsPublic) assert.False(t, channel.HasNotification) }, }, { name: "Get private channel members", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/channels/%s/members", mockChannel.ID) return http.NewRequest(http.MethodGet, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) var respBody []string err := json.Unmarshal(recorder.Body.Bytes(), &respBody) assert.NoError(t, err) assert.NotNil(t, respBody) assert.Equal(t, 1, len(respBody)) id := respBody[0] assert.Equal(t, authUser.ID, id) }, }, { name: "Delete Channel", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodDelete, "/api/channels/"+mockChannel.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Check that the channel is got deleted", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/channels/"+mockGuild.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.ChannelResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) channels := *respBody assert.Equal(t, 1, len(channels)) channel := channels[0] assert.Equal(t, "general", channel.Name) assert.NotNil(t, channel.Id) assert.NotNil(t, channel.CreatedAt) assert.NotNil(t, channel.UpdatedAt) assert.True(t, channel.IsPublic) assert.False(t, channel.HasNotification) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() request, err := tc.setupRequest() tc.setupHeaders(t, request) assert.NoError(t, err) router.ServeHTTP(rr, request) tc.checkResponse(rr) }) } } func TestMain_DMsE2E(t *testing.T) { router := setupTest(t) authUser := fixture.GetMockUser() cookie := "" mockUser := fixture.GetMockUser() dmId := "" testCases := []struct { name string setupRequest func() (*http.Request, error) setupHeaders func(t *testing.T, request *http.Request) checkResponse func(recorder *httptest.ResponseRecorder) }{ { name: "Register Account", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": authUser.Username, "password": authUser.Password, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.Contains(t, recorder.Header(), "Set-Cookie") authUser.ID = respBody.ID cookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Register Member", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": mockUser.Username, "password": mockUser.Password, "email": mockUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockUser.Username, respBody.Username) assert.Equal(t, mockUser.Email, respBody.Email) assert.Equal(t, mockUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.Image) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) mockUser.ID = respBody.ID }, }, { name: "Start a DM", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/channels/%s/dm", mockUser.ID) return http.NewRequest(http.MethodPost, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &model.DirectMessage{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody.User) assert.NotNil(t, respBody.Id) assert.Equal(t, mockUser.Username, respBody.User.Username) assert.Equal(t, mockUser.ID, respBody.User.Id) assert.Equal(t, mockUser.Image, respBody.User.Image) assert.True(t, respBody.User.IsOnline) assert.False(t, respBody.User.IsFriend) dmId = respBody.Id }, }, { name: "Get authUser's DMs", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/channels/me/dm", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.DirectMessage{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) dms := *respBody assert.Len(t, dms, 1) dm := dms[0] assert.NotNil(t, dm.User) assert.NotNil(t, dm.Id) assert.Equal(t, mockUser.Username, dm.User.Username) assert.Equal(t, mockUser.ID, dm.User.Id) assert.Equal(t, mockUser.Image, dm.User.Image) assert.True(t, dm.User.IsOnline) assert.False(t, dm.User.IsFriend) }, }, { name: "Close DM", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/channels/%s/dm", dmId) return http.NewRequest(http.MethodDelete, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Verify the user does not have any open DMs", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/channels/me/dm", nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.DirectMessage{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) dms := *respBody assert.Len(t, dms, 0) }, }, { name: "Get the already existing DM", setupRequest: func() (*http.Request, error) { reqUrl := fmt.Sprintf("/api/channels/%s/dm", mockUser.ID) return http.NewRequest(http.MethodPost, reqUrl, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") request.Header.Add("Cookie", cookie) }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &model.DirectMessage{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody.User) assert.NotNil(t, respBody.Id) assert.Equal(t, mockUser.Username, respBody.User.Username) assert.Equal(t, mockUser.ID, respBody.User.Id) assert.Equal(t, mockUser.Image, respBody.User.Image) assert.True(t, respBody.User.IsOnline) assert.False(t, respBody.User.IsFriend) assert.Equal(t, dmId, respBody.Id) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() request, err := tc.setupRequest() tc.setupHeaders(t, request) assert.NoError(t, err) router.ServeHTTP(rr, request) tc.checkResponse(rr) }) } } func TestMain_MessagesE2E(t *testing.T) { router := setupTest(t) authUser := fixture.GetMockUser() cookie := "" mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel("") mockMessage := fixture.GetMockMessage("", "") mockText := fixture.RandStringRunes(10) testCases := []struct { name string setupRequest func() (*http.Request, error) setupHeaders func(t *testing.T, request *http.Request) checkResponse func(recorder *httptest.ResponseRecorder) }{ { name: "Register Account", setupRequest: func() (*http.Request, error) { data := gin.H{ "username": authUser.Username, "password": authUser.Password, "email": authUser.Email, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.User{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, authUser.Username, respBody.Username) assert.Equal(t, authUser.Email, respBody.Email) assert.Equal(t, authUser.Image, respBody.Image) assert.True(t, respBody.IsOnline) assert.NotNil(t, respBody.ID) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.Contains(t, recorder.Header(), "Set-Cookie") authUser.ID = respBody.ID cookie = recorder.Header().Get("Set-Cookie") }, }, { name: "Create Guild", setupRequest: func() (*http.Request, error) { data := gin.H{ "name": mockGuild.Name, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.GuildResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockGuild.Name, respBody.Name) assert.Equal(t, authUser.ID, respBody.OwnerId) assert.NotNil(t, respBody.Id) assert.Nil(t, respBody.Icon) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.False(t, respBody.HasNotification) assert.NotNil(t, respBody.DefaultChannelId) mockGuild.ID = respBody.Id mockGuild.OwnerId = authUser.ID }, }, { name: "Create Channel", setupRequest: func() (*http.Request, error) { data := gin.H{ "name": mockChannel.Name, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/channels/"+mockGuild.ID, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody := &model.ChannelResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.Equal(t, mockChannel.Name, respBody.Name) assert.NotNil(t, respBody.Id) assert.NotNil(t, respBody.CreatedAt) assert.NotNil(t, respBody.UpdatedAt) assert.True(t, respBody.IsPublic) assert.False(t, respBody.HasNotification) mockChannel.ID = respBody.Id mockChannel.GuildID = &mockGuild.ID }, }, { name: "Create Message", setupRequest: func() (*http.Request, error) { data := gin.H{ "text": *mockMessage.Text, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPost, "/api/messages/"+mockChannel.ID, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusCreated, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Get channel messages", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/messages/"+mockChannel.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.MessageResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) messages := *respBody assert.Len(t, messages, 1) message := messages[0] assert.Equal(t, message.Text, mockMessage.Text) assert.NotNil(t, message.Id) assert.NotNil(t, message.CreatedAt) assert.NotNil(t, message.UpdatedAt) assert.NotNil(t, message.User) assert.Nil(t, message.Attachment) author := message.User assert.Equal(t, authUser.Username, author.Username) assert.Equal(t, authUser.ID, author.Id) assert.True(t, author.IsOnline) assert.NotNil(t, author.Image) assert.NotNil(t, author.CreatedAt) assert.NotNil(t, author.UpdatedAt) assert.Nil(t, author.Nickname) assert.Nil(t, author.Color) mockMessage.ID = message.Id }, }, { name: "Edit Message", setupRequest: func() (*http.Request, error) { data := gin.H{ "text": mockText, } reqBody, err := json.Marshal(data) assert.NoError(t, err) return http.NewRequest(http.MethodPut, "/api/messages/"+mockMessage.ID, bytes.NewBuffer(reqBody)) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Verify message got edited", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/messages/"+mockChannel.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.MessageResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) messages := *respBody assert.Len(t, messages, 1) message := messages[0] assert.Equal(t, *message.Text, mockText) assert.NotNil(t, message.Id) assert.NotNil(t, message.CreatedAt) assert.NotNil(t, message.UpdatedAt) assert.NotNil(t, message.User) assert.Nil(t, message.Attachment) author := message.User assert.Equal(t, authUser.Username, author.Username) assert.Equal(t, authUser.ID, author.Id) assert.True(t, author.IsOnline) assert.NotNil(t, author.Image) assert.NotNil(t, author.CreatedAt) assert.NotNil(t, author.UpdatedAt) assert.Nil(t, author.Nickname) assert.Nil(t, author.Color) }, }, { name: "Delete Message", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodDelete, "/api/messages/"+mockMessage.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, recorder.Body.Bytes(), respBody) }, }, { name: "Verify message got deleted", setupRequest: func() (*http.Request, error) { return http.NewRequest(http.MethodGet, "/api/messages/"+mockChannel.ID, nil) }, setupHeaders: func(t *testing.T, request *http.Request) { request.Header.Add("Cookie", cookie) request.Header.Set("Content-Type", "application/json") }, checkResponse: func(recorder *httptest.ResponseRecorder) { assert.Equal(t, http.StatusOK, recorder.Code) respBody := &[]model.MessageResponse{} err := json.Unmarshal(recorder.Body.Bytes(), respBody) assert.NoError(t, err) assert.NotNil(t, respBody) messages := *respBody assert.Len(t, messages, 0) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() request, err := tc.setupRequest() tc.setupHeaders(t, request) assert.NoError(t, err) router.ServeHTTP(rr, request) tc.checkResponse(rr) }) } } ================================================ FILE: server/go.mod ================================================ module github.com/sentrionic/valkyrie go 1.20 // +heroku goVersion go1.20 require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 github.com/aws/aws-sdk-go v1.44.289 github.com/bwmarrin/snowflake v0.3.0 github.com/disintegration/imaging v1.6.2 github.com/gin-contrib/sessions v0.0.5 github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 github.com/gin-gonic/gin v1.9.1 github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 github.com/gorilla/websocket v1.5.0 github.com/joho/godotenv v1.5.1 github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lib/pq v1.10.9 github.com/mailru/easyjson v0.7.7 // indirect github.com/matoous/go-nanoid v1.5.0 github.com/matoous/go-nanoid/v2 v2.0.0 github.com/rs/cors v1.9.0 // indirect github.com/stretchr/testify v1.8.3 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/swag v1.16.1 github.com/ulule/limiter/v3 v3.11.2 golang.org/x/crypto v0.10.0 golang.org/x/net v0.11.0 // indirect golang.org/x/sys v0.9.0 // indirect golang.org/x/tools v0.10.0 // indirect gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.1 ) require ( github.com/redis/go-redis/v9 v9.0.5 github.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950 github.com/sethvargo/go-envconfig v0.9.0 ) require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect github.com/bytedance/sonic v1.9.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.14.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.4.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/image v0.8.0 // indirect golang.org/x/text v0.10.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: server/go.sum ================================================ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go v1.44.289 h1:5CVEjiHFvdiVlKPBzv0rjG4zH/21W/onT18R5AH/qx0= github.com/aws/aws-sdk-go v1.44.289/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0= github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM= github.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 h1:dyuNlYlG1faymw39NdJddnzJICy6587tiGSVioWhYoE= github.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es= github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.4.1 h1:oKfB/FhuVtit1bBM3zNRRsZ925ZkMN3HXL+LgLUM9lE= github.com/jackc/pgx/v5 v5.4.1/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o= github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950 h1:AqLt1PEuscqbMJkmkfOw1xLlDH0VIQzrDEuOGggv0a4= github.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950/go.mod h1:gmu40DuK3SLdKUzGOUofS3UDZwyeOUy6ZjPPuaALatw= github.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE= github.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= github.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg= github.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA= github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg= golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= ================================================ FILE: server/handler/account_handler.go ================================================ package handler import ( "fmt" "github.com/gin-gonic/gin" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/sentrionic/valkyrie/model/apperrors" "log" "mime/multipart" "net/http" "strings" ) /* * AccountHandler contains all routes related to account actions (/api/account) * that the authenticated user can do */ // GetCurrent handler calls services for getting // a user's details // GetCurrent godoc // @Tags Account // @Summary Get Current User // @Produce json // @Success 200 {object} model.User // @Failure 404 {object} model.ErrorResponse // @Router /account [get] func (h *Handler) GetCurrent(c *gin.Context) { userId := c.MustGet("userId").(string) user, err := h.userService.Get(userId) if err != nil { log.Printf("Unable to find user: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, user) } type editReq struct { // Min 3, max 30 characters. Username string `form:"username"` // Must be unique Email string `form:"email"` // image/png or image/jpeg Image *multipart.FileHeader `form:"image" swaggertype:"string" format:"binary"` } //@name EditUser func (r editReq) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Email, validation.Required, is.EmailFormat), validation.Field(&r.Username, validation.Required, validation.Length(3, 30)), ) } func (r *editReq) sanitize() { r.Username = strings.TrimSpace(r.Username) r.Email = strings.TrimSpace(r.Email) r.Email = strings.ToLower(r.Email) } // Edit handler edits the users account details // Edit godoc // @Tags Account // @Summary Update Current User // @Accept mpfd // @Produce json // @Param account body editReq true "Update Account" // @Success 200 {object} model.User // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /account [put] func (h *Handler) Edit(c *gin.Context) { userId := c.MustGet("userId").(string) c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, h.MaxBodyBytes) var req editReq if ok := bindData(c, &req); !ok { return } req.sanitize() authUser, err := h.userService.Get(userId) if err != nil { e := apperrors.NewAuthorization(apperrors.InvalidSession) c.JSON(e.Status(), gin.H{ "error": e, }) return } authUser.Username = req.Username // New email, check if it's unique if authUser.Email != req.Email { inUse := h.userService.IsEmailAlreadyInUse(req.Email) if inUse { toFieldErrorResponse(c, "Email", apperrors.DuplicateEmail) return } authUser.Email = req.Email } if req.Image != nil { // Validate image mime-type is allowable mimeType := req.Image.Header.Get("Content-Type") if valid := isAllowedImageType(mimeType); !valid { toFieldErrorResponse(c, "Image", apperrors.InvalidImageType) return } directory := fmt.Sprintf("valkyrie/users/%s", authUser.ID) url, err := h.userService.ChangeAvatar(req.Image, directory) if err != nil { e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } _ = h.userService.DeleteImage(authUser.Image) authUser.Image = url } err = h.userService.UpdateAccount(authUser) if err != nil { e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, authUser) } type changeRequest struct { CurrentPassword string `json:"currentPassword"` // Min 6, max 150 characters. NewPassword string `json:"newPassword"` // Must be the same as the newPassword value. ConfirmNewPassword string `json:"confirmNewPassword"` } //@name ChangePasswordRequest func (r changeRequest) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.CurrentPassword, validation.Required, validation.Length(6, 150)), validation.Field(&r.NewPassword, validation.Required, validation.Length(6, 150)), validation.Field(&r.ConfirmNewPassword, validation.Required, validation.Length(6, 150)), ) } func (r *changeRequest) sanitize() { r.CurrentPassword = strings.TrimSpace(r.CurrentPassword) r.NewPassword = strings.TrimSpace(r.NewPassword) r.ConfirmNewPassword = strings.TrimSpace(r.ConfirmNewPassword) } // ChangePassword handler changes the user's password // ChangePassword godoc // @Tags Account // @Summary Change Current User's Password // @Accept json // @Produce json // @Param request body changeRequest true "Change Password" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /account/change-password [put] func (h *Handler) ChangePassword(c *gin.Context) { userId := c.MustGet("userId").(string) var req changeRequest // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } req.sanitize() // Check if passwords are equal if req.NewPassword != req.ConfirmNewPassword { toFieldErrorResponse(c, "password", apperrors.PasswordsDoNotMatch) return } authUser, err := h.userService.Get(userId) if err != nil { e := apperrors.NewAuthorization(apperrors.InvalidSession) c.JSON(e.Status(), gin.H{ "error": e, }) return } err = h.userService.ChangePassword(req.CurrentPassword, req.NewPassword, authUser) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } c.JSON(http.StatusOK, true) } ================================================ FILE: server/handler/account_handler_test.go ================================================ package handler import ( "bytes" "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/sentrionic/valkyrie/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "mime/multipart" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) func TestHandler_GetCurrent(t *testing.T) { // Setup gin.SetMode(gin.TestMode) t.Run("Success", func(t *testing.T) { uid := service.GenerateId() mockUserResp := fixture.GetMockUser() mockUserResp.ID = uid mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUserResp, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(uid) NewHandler(&Config{ R: router, UserService: mockUserService, }) request, err := http.NewRequest(http.MethodGet, "/api/account", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(mockUserResp) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertExpectations(t) }) t.Run("NotFound", func(t *testing.T) { uid := service.GenerateId() mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(nil, fmt.Errorf("some error down call chain")) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(uid) NewHandler(&Config{ R: router, UserService: mockUserService, }) request, err := http.NewRequest(http.MethodGet, "/api/account", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respErr := apperrors.NewNotFound("user", uid) respBody, err := json.Marshal(gin.H{ "error": respErr, }) assert.NoError(t, err) assert.Equal(t, respErr.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertExpectations(t) // assert that UserService.Get was called }) t.Run("Unauthorized", func(t *testing.T) { uid := service.GenerateId() mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(nil, nil) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, UserService: mockUserService, }) request, err := http.NewRequest(http.MethodGet, "/api/account", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockUserService.AssertNotCalled(t, "Get", uid) }) } func TestHandler_EditAccount(t *testing.T) { // Setup gin.SetMode(gin.TestMode) uid := service.GenerateId() mockUser := fixture.GetMockUser() mockUser.ID = uid t.Run("Unauthorized", func(t *testing.T) { router := getTestRouter() mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, }) rr := httptest.NewRecorder() newName := fixture.Username() newEmail := fixture.Email() form := url.Values{} form.Add("username", newName) form.Add("email", newEmail) request, _ := http.NewRequest(http.MethodPut, "/api/account", strings.NewReader(form.Encode())) request.Form = form router.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockUserService.AssertNotCalled(t, "UpdateAccount") }) t.Run("UpdateAccount success", func(t *testing.T) { router := getAuthenticatedTestRouter(uid) mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() newName := fixture.Username() newEmail := fixture.Email() body := &bytes.Buffer{} writer := multipart.NewWriter(body) _ = writer.WriteField("username", newName) _ = writer.WriteField("email", newEmail) _ = writer.Close() request, _ := http.NewRequest(http.MethodPut, "/api/account", body) request.Header.Set("Content-Type", writer.FormDataContentType()) mockUser.Username = newName mockUser.Email = newEmail UpdateAccountArgs := mock.Arguments{ mockUser, } dbImageURL := "https://website.com/696292a38f493a4283d1a308e4a11732/84d81/Profile.jpg" mockUserService. On("UpdateAccount", UpdateAccountArgs...). Run(func(args mock.Arguments) { userArg := args.Get(0).(*model.User) userArg.Image = dbImageURL }). Return(nil) router.ServeHTTP(rr, request) mockUser.Image = dbImageURL respBody, _ := json.Marshal(mockUser) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "UpdateAccount", UpdateAccountArgs...) }) t.Run("UpdateAccount Failure", func(t *testing.T) { router := getAuthenticatedTestRouter(uid) mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() form := url.Values{} form.Add("username", mockUser.Username) form.Add("email", mockUser.Email) request, _ := http.NewRequest(http.MethodPut, "/api/account", strings.NewReader(form.Encode())) request.Form = form mockError := apperrors.NewInternal() mockUserService. On("UpdateAccount", mockUser). Return(mockError) router.ServeHTTP(rr, request) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "UpdateAccount", mockUser) }) t.Run("Disallowed mimetype", func(t *testing.T) { router := getAuthenticatedTestRouter(uid) mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() multipartImageFixture := fixture.NewMultipartImage("image.txt", "mage/svg+xml") defer multipartImageFixture.Close() request, _ := http.NewRequest(http.MethodPut, "/api/account", multipartImageFixture.MultipartBody) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockUserService.AssertNotCalled(t, "ChangeAvatar") }) t.Run("Email already in use", func(t *testing.T) { router := getAuthenticatedTestRouter(uid) mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() form := url.Values{} duplicateEmail := "duplicate@example.com" form.Add("username", mockUser.Username) form.Add("email", duplicateEmail) request, _ := http.NewRequest(http.MethodPut, "/api/account", strings.NewReader(form.Encode())) request.Form = form mockUserService. On("IsEmailAlreadyInUse", duplicateEmail). Return(true) router.ServeHTTP(rr, request) respBody, _ := json.Marshal(getTestFieldErrorResponse("Email", apperrors.DuplicateEmail)) assert.Equal(t, http.StatusBadRequest, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertNotCalled(t, "UpdateAccount", mockUser) }) } func TestHandler_ChangePassword(t *testing.T) { // Setup gin.SetMode(gin.TestMode) uid := service.GenerateId() mockUser := fixture.GetMockUser() mockUser.ID = uid t.Run("Unauthorized", func(t *testing.T) { router := getTestRouter() mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "currentPassword": "password", "newPassword": "password!", "confirmNewPassword": "password!", }) assert.NoError(t, err) request, _ := http.NewRequest(http.MethodPut, "/api/account/change-password", bytes.NewBuffer(reqBody)) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockUserService.AssertNotCalled(t, "ChangePassword") }) t.Run("ChangePassword success", func(t *testing.T) { router := getAuthenticatedTestRouter(uid) mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) currentPassword := mockUser.Password newPassword := "password!" ChangePasswordArgs := mock.Arguments{ currentPassword, newPassword, mockUser, } mockUserService. On("ChangePassword", ChangePasswordArgs...). Return(nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "currentPassword": currentPassword, "newPassword": newPassword, "confirmNewPassword": newPassword, }) assert.NoError(t, err) request, _ := http.NewRequest(http.MethodPut, "/api/account/change-password", bytes.NewBuffer(reqBody)) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, _ := json.Marshal(true) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "ChangePassword", ChangePasswordArgs...) }) t.Run("ChangePassword Failure", func(t *testing.T) { router := getAuthenticatedTestRouter(uid) mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) currentPassword := mockUser.Password newPassword := "password!" ChangePasswordArgs := mock.Arguments{ currentPassword, newPassword, mockUser, } mockError := apperrors.NewInternal() mockUserService. On("ChangePassword", ChangePasswordArgs...). Return(mockError) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "currentPassword": currentPassword, "newPassword": newPassword, "confirmNewPassword": newPassword, }) assert.NoError(t, err) request, _ := http.NewRequest(http.MethodPut, "/api/account/change-password", bytes.NewBuffer(reqBody)) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "ChangePassword", ChangePasswordArgs...) }) } func TestHandler_ChangePassword_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) uid := service.GenerateId() mockUser := fixture.GetMockUser() mockUser.ID = uid router := getAuthenticatedTestRouter(uid) mockUserService := new(mocks.UserService) mockUserService.On("Get", uid).Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) password := fixture.RandStringRunes(6) confirmPassword := password testCases := []struct { name string body gin.H }{ { name: "CurrentPassword required", body: gin.H{ "newPassword": password, "confirmNewPassword": confirmPassword, }, }, { name: "NewPassword too short", body: gin.H{ "currentPassword": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(5), "confirmNewPassword": confirmPassword, }, }, { name: "NewPassword too long", body: gin.H{ "currentPassword": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(151), "confirmNewPassword": confirmPassword, }, }, { name: "NewPassword required", body: gin.H{ "currentPassword": fixture.RandStringRunes(6), "confirmNewPassword": fixture.RandStringRunes(6), }, }, { name: "ConfirmNewPassword too short", body: gin.H{ "currentPassword": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(6), "confirmNewPassword": fixture.RandStringRunes(5), }, }, { name: "ConfirmNewPassword too long", body: gin.H{ "currentPassword": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(6), "confirmNewPassword": fixture.RandStringRunes(151), }, }, { name: "ConfirmNewPassword required", body: gin.H{ "currentPassword": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(6), }, }, { name: "NewPassword and ConfirmNewPassword not equal", body: gin.H{ "currentPassword": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(6), "confirmNewPassword": fixture.RandStringRunes(6), }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() reqBody, err := json.Marshal(tc.body) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPut, "/api/account/change-password", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockUserService.AssertNotCalled(t, "ChangePassword") }) } } ================================================ FILE: server/handler/auth_handler.go ================================================ package handler import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "log" "net/http" "strings" ) /* * AuthHandler contains all routes related to account actions (/api/account) */ type registerReq struct { // Must be unique Email string `json:"email"` // Min 3, max 30 characters. Username string `json:"username"` // Min 6, max 150 characters. Password string `json:"password"` } //@name RegisterRequest func (r registerReq) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Email, validation.Required, is.EmailFormat), validation.Field(&r.Username, validation.Required, validation.Length(3, 30)), validation.Field(&r.Password, validation.Required, validation.Length(6, 150)), ) } func (r *registerReq) sanitize() { r.Username = strings.TrimSpace(r.Username) r.Email = strings.TrimSpace(r.Email) r.Email = strings.ToLower(r.Email) r.Password = strings.TrimSpace(r.Password) } // Register handler creates a new user // Register godoc // @Tags Account // @Summary Create an Account // @Accept json // @Produce json // @Param account body registerReq true "Create account" // @Success 201 {object} model.User // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /account/register [post] func (h *Handler) Register(c *gin.Context) { var req registerReq // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } req.sanitize() initial := &model.User{ Email: req.Email, Username: req.Username, Password: req.Password, } user, err := h.userService.Register(initial) if err != nil { if err.Error() == apperrors.NewBadRequest(apperrors.DuplicateEmail).Error() { toFieldErrorResponse(c, "Email", apperrors.DuplicateEmail) return } c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } setUserSession(c, user.ID) c.JSON(http.StatusCreated, user) } type loginReq struct { // Must be unique Email string `json:"email"` // Min 6, max 150 characters. Password string `json:"password"` } //@name LoginRequest func (r loginReq) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Email, validation.Required, is.EmailFormat), validation.Field(&r.Password, validation.Required, validation.Length(6, 150)), ) } func (r *loginReq) sanitize() { r.Email = strings.TrimSpace(r.Email) r.Email = strings.ToLower(r.Email) r.Password = strings.TrimSpace(r.Password) } // Login used to authenticate existent user // Login godoc // @Tags Account // @Summary User Login // @Accept json // @Produce json // @Param account body loginReq true "Login account" // @Success 200 {object} model.User // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /account/login [post] func (h *Handler) Login(c *gin.Context) { var req loginReq if ok := bindData(c, &req); !ok { return } req.sanitize() user, err := h.userService.Login(req.Email, req.Password) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } setUserSession(c, user.ID) c.JSON(http.StatusOK, user) } // Logout handler removes the current session // Logout godoc // @Tags Account // @Summary User Logout // @Accept json // @Produce json // @Param account body loginReq true "Login account" // @Success 200 {object} model.Success // @Router /account/logout [post] func (h *Handler) Logout(c *gin.Context) { c.Set("user", nil) session := sessions.Default(c) session.Set("userId", "") session.Clear() session.Options(sessions.Options{Path: "/", MaxAge: -1}) err := session.Save() if err != nil { log.Printf("error clearing session: %v\n", err.Error()) } c.JSON(http.StatusOK, true) } type forgotRequest struct { Email string `json:"email"` } //@name ForgotPasswordRequest func (r forgotRequest) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Email, validation.Required, is.EmailFormat), ) } func (r *forgotRequest) sanitize() { r.Email = strings.TrimSpace(r.Email) r.Email = strings.ToLower(r.Email) } // ForgotPassword sends a password reset email to the requested email // ForgotPassword godoc // @Tags Account // @Summary Forgot Password Request // @Accept json // @Produce json // @Param email body forgotRequest true "Forgot Password" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorsResponse // @Failure 500 {object} model.ErrorResponse // @Router /account/forgot-password [post] func (h *Handler) ForgotPassword(c *gin.Context) { var req forgotRequest if valid := bindData(c, &req); !valid { return } req.sanitize() user, err := h.userService.GetByEmail(req.Email) if err != nil { // No user with the email found if err.Error() == apperrors.NewNotFound("email", req.Email).Error() { c.JSON(http.StatusOK, true) return } e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } ctx := c.Request.Context() err = h.userService.ForgotPassword(ctx, user) if err != nil { e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, true) } type resetRequest struct { // The token the user got from the email. Token string `json:"token"` // Min 6, max 150 characters. Password string `json:"newPassword"` // Must be the same as the password value. ConfirmPassword string `json:"confirmNewPassword"` } //@name ResetPasswordRequest func (r resetRequest) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Token, validation.Required), validation.Field(&r.Password, validation.Required, validation.Length(6, 150)), validation.Field(&r.ConfirmPassword, validation.Required, validation.Length(6, 150)), ) } func (r *resetRequest) sanitize() { r.Token = strings.TrimSpace(r.Token) r.Password = strings.TrimSpace(r.Password) r.ConfirmPassword = strings.TrimSpace(r.ConfirmPassword) } // ResetPassword resets the user's password with the provided token // ResetPassword godoc // @Tags Account // @Summary Reset Password // @Accept json // @Produce json // @Param request body resetRequest true "Reset Password" // @Success 200 {object} model.User // @Failure 400 {object} model.ErrorsResponse // @Failure 500 {object} model.ErrorResponse // @Router /account/reset-password [post] func (h *Handler) ResetPassword(c *gin.Context) { var req resetRequest if valid := bindData(c, &req); !valid { return } req.sanitize() // Check if passwords match if req.Password != req.ConfirmPassword { toFieldErrorResponse(c, "Password", apperrors.PasswordsDoNotMatch) return } ctx := c.Request.Context() user, err := h.userService.ResetPassword(ctx, req.Password, req.Token) if err != nil { if err.Error() == apperrors.NewBadRequest(apperrors.InvalidResetToken).Error() { toFieldErrorResponse(c, "Token", apperrors.InvalidResetToken) return } c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } setUserSession(c, user.ID) c.JSON(http.StatusOK, user) } ================================================ FILE: server/handler/auth_handler_test.go ================================================ package handler import ( "bytes" "encoding/json" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/sentrionic/valkyrie/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "net/http" "net/http/httptest" "testing" ) func TestHandler_Register(t *testing.T) { // Setup gin.SetMode(gin.TestMode) user := fixture.GetMockUser() reqUser := &model.User{ Email: user.Email, Password: user.Password, Username: user.Username, } t.Run("Email, Username and Password Required", func(t *testing.T) { // We just want this to show that it's not called in this case mockUserService := new(mocks.UserService) mockUserService.On("Register", mock.AnythingOfType("*model.User")).Return(nil) // a response recorder for getting written http response rr := httptest.NewRecorder() // don't need a middleware as we don't yet have authorized user router := gin.Default() NewHandler(&Config{ R: router, UserService: mockUserService, }) // create a request body with empty email and password reqBody, err := json.Marshal(gin.H{ "email": "", "username": "", }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, 400, rr.Code) mockUserService.AssertNotCalled(t, "Register") }) t.Run("Invalid email", func(t *testing.T) { // We just want this to show that it's not called in this case mockUserService := new(mocks.UserService) mockUserService.On("Register", mock.AnythingOfType("*model.User")).Return(nil) // a response recorder for getting written http response rr := httptest.NewRecorder() // don't need a middleware as we don't yet have authorized user router := gin.Default() NewHandler(&Config{ R: router, UserService: mockUserService, }) // create a request body with empty email and password reqBody, err := json.Marshal(gin.H{ "email": "bob@bob", "username": "bobby", "password": "supersecret1234", }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, 400, rr.Code) mockUserService.AssertNotCalled(t, "Signup") }) t.Run("Username too short", func(t *testing.T) { // We just want this to show that it's not called in this case mockUserService := new(mocks.UserService) mockUserService.On("Register", mock.AnythingOfType("*model.User")).Return(nil) // a response recorder for getting written http response rr := httptest.NewRecorder() // don't need a middleware as we don't yet have authorized user router := gin.Default() NewHandler(&Config{ R: router, UserService: mockUserService, }) // create a request body with empty email and password reqBody, err := json.Marshal(gin.H{ "email": "bob@bob.com", "username": "bo", "password": "superpassword", }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, 400, rr.Code) mockUserService.AssertNotCalled(t, "Register") }) t.Run("Password too short", func(t *testing.T) { // We just want this to show that it's not called in this case mockUserService := new(mocks.UserService) mockUserService.On("Register", mock.AnythingOfType("*model.User")).Return(nil) // a response recorder for getting written http response rr := httptest.NewRecorder() // don't need a middleware as we don't yet have authorized user router := gin.Default() NewHandler(&Config{ R: router, UserService: mockUserService, }) // create a request body with empty email and password reqBody, err := json.Marshal(gin.H{ "email": "bob@bob.com", "username": "bobby", "password": "supe", }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, 400, rr.Code) mockUserService.AssertNotCalled(t, "Register") }) t.Run("Username too long", func(t *testing.T) { // We just want this to show that it's not called in this case mockUserService := new(mocks.UserService) mockUserService.On("Register", mock.AnythingOfType("*model.User")).Return(nil) // a response recorder for getting written http response rr := httptest.NewRecorder() // don't need a middleware as we don't yet have authorized user router := gin.Default() NewHandler(&Config{ R: router, UserService: mockUserService, }) // create a request body with empty email and password reqBody, err := json.Marshal(gin.H{ "email": "bob@bob.com", "username": "kjhasiudaiusdiuadiuagszuidgaiszugdziasgdiazgsdiazugdipas", "password": "superpassword", }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, 400, rr.Code) mockUserService.AssertNotCalled(t, "Register") }) t.Run("Error returned from UserService", func(t *testing.T) { u := &model.User{ Email: reqUser.Email, Username: reqUser.Username, Password: reqUser.Password, } mockUserService := new(mocks.UserService) mockUserService.On("Register", u).Return(nil, apperrors.NewConflict("User Already Exists", u.Email)) // a response recorder for getting written http response rr := httptest.NewRecorder() // don't need a middleware as we don't yet have authorized user router := gin.Default() NewHandler(&Config{ R: router, UserService: mockUserService, }) // create a request body with empty email and password reqBody, err := json.Marshal(gin.H{ "email": u.Email, "username": u.Username, "password": u.Password, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, 409, rr.Code) mockUserService.AssertExpectations(t) }) t.Run("Successful Creation", func(t *testing.T) { u := &model.User{ Email: reqUser.Email, Username: reqUser.Username, Password: reqUser.Password, } mockUserService := new(mocks.UserService) mockUserService. On("Register", u). Return(reqUser, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, UserService: mockUserService, }) // create a request body with empty email and password reqBody, err := json.Marshal(gin.H{ "email": u.Email, "username": u.Username, "password": u.Password, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/account/register", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(u) assert.NoError(t, err) assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertExpectations(t) }) } func TestHandler_Login(t *testing.T) { // Setup gin.SetMode(gin.TestMode) // setup mock services, gin engine/router, handler layer mockUserService := new(mocks.UserService) router := getTestRouter() NewHandler(&Config{ R: router, UserService: mockUserService, }) t.Run("Bad request data", func(t *testing.T) { // a response recorder for getting written http response rr := httptest.NewRecorder() // create a request body with invalid fields reqBody, err := json.Marshal(gin.H{ "email": "notanemail", "password": "short", }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/account/login", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockUserService.AssertNotCalled(t, "Login") }) t.Run("Error Returned from UserService.Login", func(t *testing.T) { email := "bob@bob.com" password := "pwdoesnotmatch123" mockUSArgs := mock.Arguments{ email, password, } // so we can check for a known status code mockError := apperrors.NewAuthorization("invalid email/password combo") mockUserService.On("Login", mockUSArgs...).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() // create a request body with valid fields reqBody, err := json.Marshal(gin.H{ "email": email, "password": password, }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/account/login", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockUserService.AssertCalled(t, "Login", mockUSArgs...) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) t.Run("Successful Login", func(t *testing.T) { user := fixture.GetMockUser() mockUSArgs := mock.Arguments{ user.Email, user.Password, } mockUserService.On("Login", mockUSArgs...).Return(user, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() // create a request body with valid fields reqBody, err := json.Marshal(gin.H{ "email": user.Email, "password": user.Password, }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/account/login", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(user) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "Login", mockUSArgs...) }) } func TestHandler_Logout(t *testing.T) { gin.SetMode(gin.TestMode) t.Run("Success", func(t *testing.T) { uid := service.GenerateId() rr := httptest.NewRecorder() // creates a test context for setting a user router := getAuthenticatedTestRouter(uid) NewHandler(&Config{ R: router, }) request, _ := http.NewRequest(http.MethodPost, "/api/account/logout", nil) router.ServeHTTP(rr, request) respBody, _ := json.Marshal(true) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) router.Use(func(c *gin.Context) { contextUserId, exists := c.Get("userId") assert.Equal(t, exists, false) assert.Nil(t, contextUserId) session := sessions.Default(c) id := session.Get("userId") assert.Nil(t, id) }) }) } func TestHandler_ForgotPassword(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() t.Run("ForgotPassword success", func(t *testing.T) { router := gin.Default() mockUserService := new(mocks.UserService) mockUserService.On("GetByEmail", mockUser.Email).Return(mockUser, nil) ForgotPasswordArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockUser, } mockUserService. On("ForgotPassword", ForgotPasswordArgs...). Return(nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "email": mockUser.Email, }) assert.NoError(t, err) request, _ := http.NewRequest(http.MethodPost, "/api/account/forgot-password", bytes.NewBuffer(reqBody)) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, _ := json.Marshal(true) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "ForgotPassword", ForgotPasswordArgs...) }) t.Run("ForgotPassword Failure", func(t *testing.T) { router := gin.Default() mockUserService := new(mocks.UserService) mockUserService.On("GetByEmail", mockUser.Email).Return(mockUser, nil) ForgotPasswordArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockUser, } mockError := apperrors.NewInternal() mockUserService. On("ForgotPassword", ForgotPasswordArgs...). Return(mockError) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "email": mockUser.Email, }) assert.NoError(t, err) request, _ := http.NewRequest(http.MethodPost, "/api/account/forgot-password", bytes.NewBuffer(reqBody)) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "ForgotPassword", ForgotPasswordArgs...) }) t.Run("No user found", func(t *testing.T) { router := gin.Default() mockUserService := new(mocks.UserService) mockError := apperrors.NewNotFound("email", mockUser.Email) mockUserService.On("GetByEmail", mockUser.Email).Return(&model.User{}, mockError) ForgotPasswordArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockUser, } mockUserService. On("ForgotPassword", ForgotPasswordArgs...). Return(nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "email": mockUser.Email, }) assert.NoError(t, err) request, _ := http.NewRequest(http.MethodPost, "/api/account/forgot-password", bytes.NewBuffer(reqBody)) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, _ := json.Marshal(true) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertNotCalled(t, "ForgotPassword", ForgotPasswordArgs...) }) } func TestHandler_ForgotPassword_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() router := gin.Default() mockUserService := new(mocks.UserService) mockUserService.On("GetByEmail", mockUser.Email).Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) testCases := []struct { name string body gin.H }{ { name: "Email required", body: gin.H{}, }, { name: "Invalid Email", body: gin.H{ "email": "invalidemail", }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() reqBody, err := json.Marshal(tc.body) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/account/forgot-password", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockUserService.AssertNotCalled(t, "ForgotPassword") }) } } func TestHandler_ResetPassword(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() token := fixture.RandStringRunes(18) t.Run("ResetPassword success", func(t *testing.T) { mockUserService := new(mocks.UserService) router := getTestRouter() ResetPasswordArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockUser.Password, token, } mockUserService. On("ResetPassword", ResetPasswordArgs...). Return(mockUser, nil) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "token": token, "newPassword": mockUser.Password, "confirmNewPassword": mockUser.Password, }) assert.NoError(t, err) request, _ := http.NewRequest(http.MethodPost, "/api/account/reset-password", bytes.NewBuffer(reqBody)) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, _ := json.Marshal(mockUser) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "ResetPassword", ResetPasswordArgs...) }) t.Run("ResetPassword Failure", func(t *testing.T) { mockUserService := new(mocks.UserService) router := getTestRouter() ResetPasswordArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockUser.Password, token, } mockError := apperrors.NewInternal() mockUserService. On("ResetPassword", ResetPasswordArgs...). Return(nil, mockError) NewHandler(&Config{ R: router, UserService: mockUserService, MaxBodyBytes: 4 * 1024 * 1024, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "token": token, "newPassword": mockUser.Password, "confirmNewPassword": mockUser.Password, }) assert.NoError(t, err) request, _ := http.NewRequest(http.MethodPost, "/api/account/reset-password", bytes.NewBuffer(reqBody)) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockUserService.AssertCalled(t, "ResetPassword", ResetPasswordArgs...) }) } func TestHandler_ResetPassword_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) router := gin.Default() mockUserService := new(mocks.UserService) NewHandler(&Config{ R: router, UserService: mockUserService, }) password := fixture.RandStringRunes(6) confirmPassword := password testCases := []struct { name string body gin.H }{ { name: "Token required", body: gin.H{ "newPassword": password, "confirmNewPassword": password, }, }, { name: "Password required", body: gin.H{ "token": fixture.RandStringRunes(18), "confirmNewPassword": password, }, }, { name: "Password too short", body: gin.H{ "token": fixture.RandStringRunes(18), "newPassword": fixture.RandStringRunes(5), "confirmNewPassword": confirmPassword, }, }, { name: "NewPassword too long", body: gin.H{ "token": fixture.RandStringRunes(16), "newPassword": fixture.RandStringRunes(151), "confirmNewPassword": confirmPassword, }, }, { name: "ConfirmNewPassword too short", body: gin.H{ "token": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(6), "confirmNewPassword": fixture.RandStringRunes(5), }, }, { name: "ConfirmNewPassword too long", body: gin.H{ "currentPassword": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(6), "confirmNewPassword": fixture.RandStringRunes(151), }, }, { name: "ConfirmNewPassword required", body: gin.H{ "token": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(6), }, }, { name: "NewPassword and ConfirmNewPassword not equal", body: gin.H{ "token": fixture.RandStringRunes(6), "newPassword": fixture.RandStringRunes(6), "confirmNewPassword": fixture.RandStringRunes(6), }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() reqBody, err := json.Marshal(tc.body) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/account/reset-password", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockUserService.AssertNotCalled(t, "ResetPassword") }) } } ================================================ FILE: server/handler/bind_data.go ================================================ package handler import ( "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "net/http" "strings" "github.com/gin-gonic/gin" ) // Request contains the validate function which validates the request with bindData type Request interface { validate() error } // bindData is helper function, returns false if data is not bound func bindData(c *gin.Context, req Request) bool { // Bind incoming json to struct and check for validation errors if err := c.ShouldBind(req); err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return false } if err := req.validate(); err != nil { errors := strings.Split(err.Error(), ";") fErrors := make([]model.FieldError, 0) for _, e := range errors { split := strings.Split(e, ":") er := model.FieldError{ Field: strings.TrimSpace(split[0]), Message: strings.TrimSpace(split[1]), } fErrors = append(fErrors, er) } c.JSON(http.StatusBadRequest, gin.H{ "errors": fErrors, }) return false } return true } ================================================ FILE: server/handler/channel_handler.go ================================================ package handler import ( "fmt" "github.com/gin-gonic/gin" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "log" "net/http" "strings" ) /* * ChannelHandler contains all routes related to channel actions (/api/channels) */ // GuildChannels returns the given guild's channels // GuildChannels godoc // @Tags Channels // @Summary Get Guild Channels // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {array} model.ChannelResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /channels/{guildId} [get] func (h *Handler) GuildChannels(c *gin.Context) { guildId := c.Param("id") userId := c.MustGet("userId").(string) guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Only get the channels if the user is a member if !isMember(guild, userId) { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } channels, err := h.channelService.GetChannels(userId, guildId) if err != nil { log.Printf("Unable to find channels for guild id: %v\n%v", guildId, err) e := apperrors.NewNotFound("channels", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, channels) } // channelReq specifies the input form for creating a channel // IsPublic and Members do not need to be specified if you want // to create a public channel type channelReq struct { // Channel Name. 3 to 30 character Name string `json:"name"` // Default is true IsPublic *bool `json:"isPublic"` // Array of memberIds Members []string `json:"members"` } //@name ChannelRequest func (r channelReq) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Name, validation.Required, validation.Length(3, 30)), ) } func (r *channelReq) sanitize() { r.Name = strings.TrimSpace(r.Name) } // CreateChannel creates a channel for the given guild param // CreateChannel godoc // @Tags Channels // @Summary Create Channel // @Accepts json // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {array} model.ChannelResponse // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /channels/{guildId} [post] func (h *Handler) CreateChannel(c *gin.Context) { var req channelReq // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } req.sanitize() userId := c.MustGet("userId").(string) guildId := c.Param("id") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the server already has 50 channels if len(guild.Channels) >= model.MaximumChannels { e := apperrors.NewBadRequest(apperrors.ChannelLimitError) c.JSON(e.Status(), gin.H{ "error": e, }) return } channelParams := model.Channel{ Name: req.Name, IsPublic: true, GuildID: &guildId, } // Channel is private if req.IsPublic != nil && !*req.IsPublic { channelParams.IsPublic = false // Add the current user to the members if they are not in there if !containsUser(req.Members, userId) { req.Members = append(req.Members, userId) } members, err := h.guildService.FindUsersByIds(req.Members, guildId) if err != nil { e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } // Create private channel members channelParams.PCMembers = append(channelParams.PCMembers, *members...) } channel, err := h.channelService.CreateChannel(&channelParams) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } guild.Channels = append(guild.Channels, *channel) if err = h.guildService.UpdateGuild(guild); err != nil { log.Printf("Failed to update guild: %v\n", err.Error()) e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } response := channel.SerializeChannel() // Emit the new channel to the guild members if channel.IsPublic { h.socketService.EmitNewChannel(guildId, &response) // Emit to private channel members } else { h.socketService.EmitNewPrivateChannel(req.Members, &response) } c.JSON(http.StatusCreated, response) } // PrivateChannelMembers returns the ids of all members // that are part of the channel // PrivateChannelMembers godoc // @Tags Channels // @Summary Get Members of the given Channel // @Produce json // @Param channelId path string true "Channel ID" // @Success 200 {array} string // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /channels/{channelId}/members [get] func (h *Handler) PrivateChannelMembers(c *gin.Context) { channelId := c.Param("id") userId := c.MustGet("userId").(string) channel, err := h.channelService.Get(channelId) if err != nil { e := apperrors.NewNotFound("channel", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if channel.GuildID == nil { e := apperrors.NewNotFound("channel", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } guild, err := h.guildService.GetGuild(*channel.GuildID) if err != nil { e := apperrors.NewNotFound("channel", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Public channels do not have any private members if channel.IsPublic { var empty = make([]string, 0) c.JSON(http.StatusOK, empty) return } members, err := h.channelService.GetPrivateChannelMembers(channelId) if err != nil { log.Printf("Unable to find members for channel: %v\n%v", channelId, err) e := apperrors.NewNotFound("members", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, members) } // DirectMessages returns a list of the current users DMs // DirectMessages godoc // @Tags Channels // @Summary Get User's DMs // @Produce json // @Success 200 {array} model.DirectMessage // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /channels/me/dm [get] func (h *Handler) DirectMessages(c *gin.Context) { userId := c.MustGet("userId").(string) channels, err := h.channelService.GetDirectMessages(userId) if err != nil { log.Printf("Unable to find dms for user id: %v\n%v", userId, err) e := apperrors.NewNotFound("dms", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // If the user does not have any dms, return an empty array if len(*channels) == 0 { var empty = make([]model.DirectMessage, 0) c.JSON(http.StatusOK, empty) return } c.JSON(http.StatusOK, channels) } // GetOrCreateDM gets the DM with the given member and creates it // if it does not already exist // DirectMessages godoc // @Tags Channels // @Summary Get or Create DM // @Produce json // @Param channelId path string true "Member ID" // @Success 200 {object} model.DirectMessage // @Failure 400 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /channels/{channelId}/dm [post] func (h *Handler) GetOrCreateDM(c *gin.Context) { userId := c.MustGet("userId").(string) memberId := c.Param("id") if userId == memberId { e := apperrors.NewBadRequest(apperrors.DMYourselfError) c.JSON(e.Status(), gin.H{ "error": e, }) return } member, err := h.friendService.GetMemberById(memberId) if err != nil { log.Printf("Unable to find member for id: %v\n%v", memberId, err) e := apperrors.NewNotFound("member", memberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // check if dm channel already exists with these members dmId, err := h.channelService.GetDirectMessageChannel(userId, memberId) if err != nil { log.Printf("Unable to find or create dms for user id: %v\n%v", userId, err) e := apperrors.NewNotFound("dms", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // dm already exists if dmId != nil && *dmId != "" { _ = h.channelService.SetDirectMessageStatus(*dmId, userId, true) c.JSON(http.StatusOK, toDMChannel(member, *dmId, userId)) return } // Create the dm channel between the current user and the member id := fmt.Sprintf("%s-%s", userId, memberId) channelParams := model.Channel{ Name: id, IsPublic: false, IsDM: true, } channel, err := h.channelService.CreateChannel(&channelParams) // Create the DM channel if err != nil { log.Printf("Failed to create channel: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Add the users to it ids := []string{userId, memberId} err = h.channelService.AddDMChannelMembers(ids, channel.ID, userId) if err != nil { log.Printf("Failed to create channel: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } c.JSON(http.StatusOK, toDMChannel(member, channel.ID, userId)) } // toDMChannel returns the DM response for the given channel and member func toDMChannel(member *model.User, channelId string, userId string) model.DirectMessage { return model.DirectMessage{ Id: channelId, User: model.DMUser{ Id: member.ID, Username: member.Username, Image: member.Image, IsOnline: member.IsOnline, IsFriend: isFriend(member, userId), }, } } // EditChannel edits the specified channel // EditChannel godoc // @Tags Channels // @Summary Edit Channel // @Accepts json // @Produce json // @Param channelId path string true "Channel ID" // @Param request body channelReq true "Edit Channel" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /channels/{channelId} [put] func (h *Handler) EditChannel(c *gin.Context) { var req channelReq // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } req.sanitize() userId := c.MustGet("userId").(string) channelId := c.Param("id") channel, err := h.channelService.Get(channelId) if err != nil { e := apperrors.NewNotFound("channel", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } guild, err := h.guildService.GetGuild(*channel.GuildID) if err != nil { e := apperrors.NewNotFound("guild", *channel.GuildID) c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } isPublic := true if req.IsPublic != nil { isPublic = *req.IsPublic } // Used to be private and now is public if isPublic && !channel.IsPublic { err = h.channelService.CleanPCMembers(channelId) if err != nil { log.Printf("error removing pc members: %v", err) } } channel.IsPublic = isPublic channel.Name = req.Name // Member Changes if !isPublic { // Check if the array contains the current member if !containsUser(req.Members, userId) { req.Members = append(req.Members, userId) } // Current members of the channel current := make([]string, 0) for _, member := range channel.PCMembers { current = append(current, member.ID) } // Newly added members newMembers := difference(req.Members, current) // Members that got removed toRemove := difference(current, req.Members) err = h.channelService.AddPrivateChannelMembers(newMembers, channelId) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } err = h.channelService.RemovePrivateChannelMembers(toRemove, channelId) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } } if err = h.channelService.UpdateChannel(channel); err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit the channel changes to the guild members response := channel.SerializeChannel() h.socketService.EmitEditChannel(*channel.GuildID, &response) c.JSON(http.StatusOK, true) } // difference returns the elements in `a` that aren't in `b`. func difference(a, b []string) []string { mb := make(map[string]struct{}, len(b)) for _, x := range b { mb[x] = struct{}{} } var diff []string for _, x := range a { if _, found := mb[x]; !found { diff = append(diff, x) } } return diff } // DeleteChannel removes the given channel from the guild // DeleteChannel godoc // @Tags Channels // @Summary Delete Channel // @Produce json // @Param id path string true "Channel ID" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /channels/{id} [delete] func (h *Handler) DeleteChannel(c *gin.Context) { userId := c.MustGet("userId").(string) channelId := c.Param("id") channel, err := h.channelService.Get(channelId) if err != nil { e := apperrors.NewNotFound("channel", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } guild, err := h.guildService.GetGuild(*channel.GuildID) if err != nil { e := apperrors.NewNotFound("channel", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the guild has the minimum amount of channels if len(guild.Channels) <= model.MinimumChannels { e := apperrors.NewBadRequest(apperrors.OneChannelRequired) c.JSON(e.Status(), gin.H{ "error": e, }) return } if err = h.channelService.DeleteChannel(channel); err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit signal to remove the channel from the guild h.socketService.EmitDeleteChannel(channel) c.JSON(http.StatusOK, true) } // CloseDM closes the DM on the current users side // CloseDM godoc // @Tags Channels // @Summary Close DM // @Produce json // @Param id path string true "DM Channel ID" // @Success 200 {object} model.Success // @Failure 404 {object} model.ErrorResponse // @Router /channels/{id}/dm [delete] func (h *Handler) CloseDM(c *gin.Context) { userId := c.MustGet("userId").(string) channelId := c.Param("id") dmId, err := h.channelService.GetDMByUserAndChannel(userId, channelId) if err != nil || dmId == "" { log.Printf("Unable to find or create dms for user id: %v\n%v", userId, err) e := apperrors.NewNotFound("dms", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } _ = h.channelService.SetDirectMessageStatus(channelId, userId, false) c.JSON(http.StatusOK, true) } // containsUser checks if the array contains the user func containsUser(members []string, userId string) bool { for _, m := range members { if m == userId { return true } } return false } ================================================ FILE: server/handler/channel_handler_test.go ================================================ package handler import ( "bytes" "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "net/http" "net/http/httptest" "testing" ) func TestHandler_GuildChannels(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful Fetch", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuild.Members = append(mockGuild.Members, *authUser) response := make([]model.ChannelResponse, 0) for i := 0; i < 5; i++ { mockChannel := fixture.GetMockChannel(mockGuild.ID) response = append(response, mockChannel.SerializeChannel()) } mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("GetChannels", authUser.ID, mockGuild.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(response) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Guild not found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("guild", id) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", id).Return(nil, mockError) mockChannelService := new(mocks.ChannelService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s", id) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", id) mockChannelService.AssertNotCalled(t, "GetChannels") }) t.Run("Not a member of the guild", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockError := apperrors.NewNotFound("guild", mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockChannelService.AssertNotCalled(t, "GetChannels") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockChannelService.AssertNotCalled(t, "GetChannels") }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuild.Members = append(mockGuild.Members, *authUser) mockError := apperrors.NewNotFound("channels", mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("GetChannels", authUser.ID, mockGuild.ID).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) } func TestHandler_CreateChannel(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful channel creation", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("UpdateGuild", mockGuild).Return(nil) mockChannelService := new(mocks.ChannelService) channelParams := &model.Channel{ Name: mockChannel.Name, IsPublic: true, GuildID: &mockGuild.ID, } mockChannelService.On("CreateChannel", channelParams).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) response := mockChannel.SerializeChannel() mockSocketService.On("EmitNewChannel", mockGuild.ID, &response) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(response) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Guild not found", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", mockGuild.ID) mockGuildService.On("GetGuild", mockGuild.ID).Return(nil, mockError) mockChannelService := new(mocks.ChannelService) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UpdateGuild") mockChannelService.AssertNotCalled(t, "CreateChannel") mockSocketService.AssertNotCalled(t, "EmitNewChannel") }) t.Run("Guild already has the maximum number of channels", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) for i := 0; i < model.MaximumChannels; i++ { channel := fixture.GetMockChannel(mockGuild.ID) mockGuild.Channels = append(mockGuild.Channels, *channel) } mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewBadRequest(apperrors.ChannelLimitError) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UpdateGuild") mockChannelService.AssertNotCalled(t, "CreateChannel") mockSocketService.AssertNotCalled(t, "EmitNewChannel") }) t.Run("Not the guild owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UpdateGuild") mockChannelService.AssertNotCalled(t, "CreateChannel") mockSocketService.AssertNotCalled(t, "EmitNewChannel") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) mockSocketService := new(mocks.SocketService) router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockChannelService.AssertNotCalled(t, "CreateChannel") mockSocketService.AssertNotCalled(t, "EmitNewChannel") }) t.Run("Server Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) channelParams := &model.Channel{ Name: mockChannel.Name, IsPublic: true, GuildID: &mockGuild.ID, } mockError := apperrors.NewInternal() mockChannelService.On("CreateChannel", channelParams).Return(nil, mockError) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UpdateGuild") mockChannelService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitNewChannel") }) t.Run("Successful private channel creation", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) members := make([]model.User, 0) members = append(members, *authUser) reqMembers := []string{authUser.ID} mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("UpdateGuild", mockGuild).Return(nil) mockGuildService.On("FindUsersByIds", reqMembers, mockGuild.ID).Return(&members, nil) mockChannelService := new(mocks.ChannelService) channelParams := &model.Channel{ Name: mockChannel.Name, IsPublic: false, GuildID: &mockGuild.ID, } channelParams.PCMembers = append(channelParams.PCMembers, *authUser) mockChannelService.On("CreateChannel", channelParams).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) response := mockChannel.SerializeChannel() mockSocketService.On("EmitNewChannel", mockGuild.ID, &response) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, "isPublic": false, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(response) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) } func TestHandler_CreateChannel_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() router := getAuthenticatedTestRouter(mockUser.ID) mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) testCases := []struct { name string body gin.H }{ { name: "Name required", body: gin.H{}, }, { name: "Name too short", body: gin.H{ "name": fixture.RandStr(2), }, }, { name: "Name too long", body: gin.H{ "name": fixture.RandStr(31), }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() reqBody, err := json.Marshal(tc.body) assert.NoError(t, err) url := fmt.Sprintf("/api/channels/%s", fixture.RandID()) request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "GetGuild") mockChannelService.AssertNotCalled(t, "CreateChannel") }) } } func TestHandler_PrivateChannelMembers(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful Fetch", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockChannel.IsPublic = false mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) response := make([]string, 0) for i := 0; i < 5; i++ { mockUser := fixture.GetMockUser() response = append(response, mockUser.ID) } mockChannelService.On("GetPrivateChannelMembers", mockChannel.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s/members", mockChannel.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(response) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Guild not found", func(t *testing.T) { mockChannel := fixture.GetMockChannel("") mockChannel.IsPublic = false mockError := apperrors.NewNotFound("channel", mockChannel.ID) mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s/members", mockChannel.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockGuildService.AssertNotCalled(t, "GetGuild") mockChannelService.AssertNotCalled(t, "GetPrivateChannelMembers") }) t.Run("Channel not found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("channel", id) mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", id).Return(nil, mockError) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s/members", id) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", id) mockGuildService.AssertNotCalled(t, "GetGuild") mockChannelService.AssertNotCalled(t, "GetPrivateChannelMembers") }) t.Run("Not the guild owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockChannel.IsPublic = false mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s/members", mockChannel.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) e := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, err := json.Marshal(gin.H{ "error": e, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, e.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockChannelService.AssertNotCalled(t, "GetPrivateChannelMembers") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockChannel.IsPublic = false mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s/members", mockChannel.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertNotCalled(t, "Get") mockGuildService.AssertNotCalled(t, "GetGuild") mockChannelService.AssertNotCalled(t, "GetPrivateChannelMembers") }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockChannel.IsPublic = false mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockError := apperrors.NewNotFound("members", mockChannel.ID) mockChannelService.On("GetPrivateChannelMembers", mockChannel.ID).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) url := fmt.Sprintf("/api/channels/%s/members", mockChannel.ID) request, err := http.NewRequest(http.MethodGet, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) } func TestHandler_DirectMessages(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Success", func(t *testing.T) { mockChannelService := new(mocks.ChannelService) response := make([]model.DirectMessage, 0) for i := 0; i < 5; i++ { user := fixture.GetMockUser() response = append(response, toDMChannel(user, fixture.RandID(), authUser.ID)) } mockChannelService.On("GetDirectMessages", authUser.ID).Return(&response, nil) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() request, err := http.NewRequest(http.MethodGet, "/api/channels/me/dm", nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(response) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockChannelService := new(mocks.ChannelService) router := getTestRouter() NewHandler(&Config{ R: router, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() request, err := http.NewRequest(http.MethodGet, "/api/channels/me/dm", nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertNotCalled(t, "GetDirectMessages") }) t.Run("Error", func(t *testing.T) { mockChannelService := new(mocks.ChannelService) mockError := apperrors.NewNotFound("dms", authUser.ID) mockChannelService.On("GetDirectMessages", authUser.ID).Return(nil, mockError) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() request, err := http.NewRequest(http.MethodGet, "/api/channels/me/dm", nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) }) } func TestHandler_GetOrCreateDM(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully returned an already existing DM", func(t *testing.T) { mockUser := fixture.GetMockUser() dmId := fixture.RandID() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("GetDirectMessageChannel", authUser.ID, mockUser.ID).Return(&dmId, nil) mockChannelService.On("SetDirectMessageStatus", dmId, authUser.ID, true).Return(nil) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(toDMChannel(mockUser, dmId, authUser.ID)) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockChannelService.AssertNotCalled(t, "CreateChannel") }) t.Run("Successfully returned a new DM", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("GetDirectMessageChannel", authUser.ID, mockUser.ID).Return(nil, nil) mockDM := fixture.GetMockDMChannel() channelParams := &model.Channel{ Name: fmt.Sprintf("%s-%s", authUser.ID, mockUser.ID), IsPublic: false, IsDM: true, } mockChannelService.On("CreateChannel", channelParams).Return(mockDM, nil) ids := []string{authUser.ID, mockUser.ID} mockArgs := mock.Arguments{ ids, mockDM.ID, authUser.ID, } mockChannelService.On("AddDMChannelMembers", mockArgs...).Return(nil) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(toDMChannel(mockUser, mockDM.ID, authUser.ID)) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockChannelService.AssertExpectations(t) }) t.Run("Member not found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("member", id) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", id).Return(nil, mockError) mockChannelService := new(mocks.ChannelService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", id) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertCalled(t, "GetMemberById", id) mockChannelService.AssertNotCalled(t, "GetDirectMessageChannel") mockChannelService.AssertNotCalled(t, "CreateChannel") }) t.Run("Member and AuthUser are the same", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockChannelService := new(mocks.ChannelService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", authUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") e := apperrors.NewBadRequest(apperrors.DMYourselfError) respBody, _ := json.Marshal(gin.H{ "error": e, }) router.ServeHTTP(rr, request) assert.Equal(t, e.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertNotCalled(t, "GetMemberById") mockChannelService.AssertNotCalled(t, "GetDirectMessageChannel") mockChannelService.AssertNotCalled(t, "CreateChannel") }) t.Run("Unauthorized", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockChannelService := new(mocks.ChannelService) router := getTestRouter() NewHandler(&Config{ R: router, FriendService: mockFriendService, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", fixture.RandID()) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertNotCalled(t, "GetMemberById") mockChannelService.AssertNotCalled(t, "GetDirectMessageChannel") mockChannelService.AssertNotCalled(t, "CreateChannel") }) t.Run("Server Error", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("GetDirectMessageChannel", authUser.ID, mockUser.ID).Return(nil, nil) mockDM := fixture.GetMockDMChannel() channelParams := &model.Channel{ Name: fmt.Sprintf("%s-%s", authUser.ID, mockUser.ID), IsPublic: false, IsDM: true, } mockChannelService.On("CreateChannel", channelParams).Return(mockDM, nil) ids := []string{authUser.ID, mockUser.ID} mockArgs := mock.Arguments{ ids, mockDM.ID, authUser.ID, } mockError := apperrors.NewInternal() mockChannelService.On("AddDMChannelMembers", mockArgs...).Return(mockError) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockChannelService.AssertExpectations(t) }) } func TestHandler_EditChannel(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful edited channel", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("UpdateChannel", mockChannel).Return(nil) mockSocketService := new(mocks.SocketService) response := mockChannel.SerializeChannel() mockSocketService.On("EmitEditChannel", mockGuild.ID, &response) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Guild not found", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockError := apperrors.NewNotFound("guild", mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(nil, mockError) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockChannelService.AssertNotCalled(t, "UpdateChannel") mockSocketService.AssertNotCalled(t, "EmitEditChannel") }) t.Run("Channel not found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("channel", id) mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", id).Return(nil, mockError) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": fixture.RandStr(8), }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", id) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", id) mockGuildService.AssertNotCalled(t, "GetGuild") mockChannelService.AssertNotCalled(t, "UpdateChannel") mockSocketService.AssertNotCalled(t, "EmitEditChannel") }) t.Run("Not the guild owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") e := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, _ := json.Marshal(gin.H{ "error": e, }) router.ServeHTTP(rr, request) assert.Equal(t, e.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockChannelService.AssertNotCalled(t, "UpdateChannel") mockSocketService.AssertNotCalled(t, "EmitEditChannel") }) t.Run("Unauthorized", func(t *testing.T) { id := fixture.RandID() mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) mockSocketService := new(mocks.SocketService) router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": fixture.RandStr(8), }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", id) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertNotCalled(t, "Get") mockGuildService.AssertNotCalled(t, "GetGuild") mockChannelService.AssertNotCalled(t, "UpdateChannel") mockSocketService.AssertNotCalled(t, "EmitEditChannel") }) t.Run("Server Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockError := apperrors.NewInternal() mockChannelService.On("UpdateChannel", mockChannel).Return(mockError) mockSocketService := new(mocks.SocketService) response := mockChannel.SerializeChannel() mockSocketService.On("EmitEditChannel", mockGuild.ID, &response) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitEditChannel") }) t.Run("Private channel made public", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockChannel.IsPublic = false mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) response := mockChannel.SerializeChannel() mockChannelService.On("CleanPCMembers", mockChannel.ID).Return(nil) mockChannelService.On("UpdateChannel", mockChannel). Run(func(args mock.Arguments) { mockChannel.IsPublic = true response = mockChannel.SerializeChannel() }). Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitEditChannel", mockGuild.ID, &response) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, "isPublic": true, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Channel made private", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("AddPrivateChannelMembers", []string{authUser.ID}, mockChannel.ID).Return(nil) mockChannelService.On("RemovePrivateChannelMembers", []string(nil), mockChannel.ID).Return(nil) response := mockChannel.SerializeChannel() mockChannelService.On("UpdateChannel", mockChannel). Run(func(args mock.Arguments) { mockChannel.IsPublic = false response = mockChannel.SerializeChannel() }). Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitEditChannel", mockGuild.ID, &response) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() reqBody, err := json.Marshal(gin.H{ "name": mockChannel.Name, "isPublic": false, }) assert.NoError(t, err) // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) } func TestHandler_EditChannel_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() router := getAuthenticatedTestRouter(mockUser.ID) mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) testCases := []struct { name string body gin.H }{ { name: "Name required", body: gin.H{}, }, { name: "Name too short", body: gin.H{ "name": fixture.RandStr(2), }, }, { name: "Name too long", body: gin.H{ "name": fixture.RandStr(31), }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() reqBody, err := json.Marshal(tc.body) assert.NoError(t, err) url := fmt.Sprintf("/api/channels/%s", fixture.RandID()) request, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "GetGuild") mockChannelService.AssertNotCalled(t, "CreateChannel") }) } } func TestHandler_DeleteChannel(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully deleted", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuild.Channels = append(mockGuild.Channels, *mockChannel) mockGuild.Channels = append(mockGuild.Channels, *fixture.GetMockChannel(mockGuild.ID)) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("DeleteChannel", mockChannel).Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitDeleteChannel", mockChannel) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Channel not found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("channel", id) mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", id).Return(nil, mockError) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", id) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", id) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "DeleteChannel") mockSocketService.AssertNotCalled(t, "EmitDeleteChannel") }) t.Run("Guild not found", func(t *testing.T) { mockChannel := fixture.GetMockChannel(fixture.RandID()) mockError := apperrors.NewNotFound("channel", mockChannel.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", *mockChannel.GuildID).Return(nil, mockError) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockGuildService.AssertCalled(t, "GetGuild", *mockChannel.GuildID) mockGuildService.AssertNotCalled(t, "DeleteChannel") mockSocketService.AssertNotCalled(t, "EmitDeleteChannel") }) t.Run("Not the guild owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", *mockChannel.GuildID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") e := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, _ := json.Marshal(gin.H{ "error": e, }) router.ServeHTTP(rr, request) assert.Equal(t, e.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockGuildService.AssertCalled(t, "GetGuild", *mockChannel.GuildID) mockGuildService.AssertNotCalled(t, "DeleteChannel") mockSocketService.AssertNotCalled(t, "EmitDeleteChannel") }) t.Run("Channel is last channel of the guild", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuild.Channels = append(mockGuild.Channels, *mockChannel) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", *mockChannel.GuildID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewBadRequest(apperrors.OneChannelRequired) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockGuildService.AssertCalled(t, "GetGuild", *mockChannel.GuildID) mockGuildService.AssertNotCalled(t, "DeleteChannel") mockSocketService.AssertNotCalled(t, "EmitDeleteChannel") }) t.Run("Unauthorized", func(t *testing.T) { id := fixture.RandID() mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) mockSocketService := new(mocks.SocketService) router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", id) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertNotCalled(t, "Get") mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "DeleteChannel") mockSocketService.AssertNotCalled(t, "EmitDeleteChannel") }) t.Run("Server Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuild.Channels = append(mockGuild.Channels, *mockChannel) mockGuild.Channels = append(mockGuild.Channels, *fixture.GetMockChannel(mockGuild.ID)) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockError := apperrors.NewInternal() mockChannelService.On("DeleteChannel", mockChannel).Return(mockError) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s", mockChannel.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) } func TestHandler_CloseDM(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully close", func(t *testing.T) { channelId := fixture.RandID() dmId := fixture.RandID() mockChannelService := new(mocks.ChannelService) findArgs := mock.Arguments{ authUser.ID, channelId, } mockChannelService.On("GetDMByUserAndChannel", findArgs...).Return(dmId, nil) setArgs := mock.Arguments{ channelId, authUser.ID, false, } mockChannelService.On("SetDirectMessageStatus", setArgs...).Return(nil) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", channelId) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) }) t.Run("DM not found", func(t *testing.T) { channelId := fixture.RandID() mockChannelService := new(mocks.ChannelService) findArgs := mock.Arguments{ authUser.ID, channelId, } mockError := apperrors.NewNotFound("dms", authUser.ID) mockChannelService.On("GetDMByUserAndChannel", findArgs...).Return("", mockError) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", channelId) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "GetDMByUserAndChannel", findArgs...) mockChannelService.AssertNotCalled(t, "SetDirectMessageStatus") }) t.Run("Unauthorized", func(t *testing.T) { channelId := fixture.RandID() mockChannelService := new(mocks.ChannelService) router := getTestRouter() NewHandler(&Config{ R: router, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", channelId) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertNotCalled(t, "GetDMByUserAndChannel") mockChannelService.AssertNotCalled(t, "SetDirectMessageStatus") }) t.Run("Server Error", func(t *testing.T) { channelId := fixture.RandID() dmId := fixture.RandID() mockChannelService := new(mocks.ChannelService) findArgs := mock.Arguments{ authUser.ID, channelId, } mockChannelService.On("GetDMByUserAndChannel", findArgs...).Return(dmId, nil) setArgs := mock.Arguments{ channelId, authUser.ID, false, } mockError := apperrors.NewInternal() mockChannelService.On("SetDirectMessageStatus", setArgs...).Return(mockError) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, }) rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader url := fmt.Sprintf("/api/channels/%s/dm", channelId) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) }) } ================================================ FILE: server/handler/friend_handler.go ================================================ package handler import ( "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "log" "net/http" ) /* * FriendHandler contains all routes related to friend actions (/api/account) */ // GetUserFriends returns the current users friends // GetUserFriends godoc // @Tags Friends // @Summary Get Current User's Friends // @Produce json // @Success 200 {array} model.Friend // @Failure 404 {object} model.ErrorResponse // @Router /account/me/friends [get] func (h *Handler) GetUserFriends(c *gin.Context) { userId := c.MustGet("userId").(string) friends, err := h.friendService.GetFriends(userId) if err != nil { log.Printf("Unable to find friends for id: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, friends) } // GetUserRequests returns the current users friend requests // GetUserRequests godoc // @Tags Friends // @Summary Get Current User's Friend Requests // @Produce json // @Success 200 {array} model.FriendRequest // @Failure 404 {object} model.ErrorResponse // @Router /account/me/pending [get] func (h *Handler) GetUserRequests(c *gin.Context) { userId := c.MustGet("userId").(string) requests, err := h.friendService.GetRequests(userId) if err != nil { log.Printf("Unable to find requests for id: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, requests) } // SendFriendRequest sends a friend request to the given member param // SendFriendRequest godoc // @Tags Friends // @Summary Send Friend Request // @Produce json // @Param memberId path string true "User ID" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /account/{memberId}/friend [post] func (h *Handler) SendFriendRequest(c *gin.Context) { userId := c.MustGet("userId").(string) memberId := c.Param("memberId") if userId == memberId { e := apperrors.NewBadRequest(apperrors.AddYourselfError) c.JSON(e.Status(), gin.H{ "error": e, }) return } authUser, err := h.friendService.GetMemberById(userId) if err != nil { log.Printf("Unable to find user: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } member, err := h.friendService.GetMemberById(memberId) if err != nil { log.Printf("Unable to find user: %v\n%v", memberId, err) e := apperrors.NewNotFound("user", memberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if they are already friends and no request exists if !isFriend(authUser, member.ID) && !containsRequest(authUser, member) { authUser.Requests = append(authUser.Requests, *member) err = h.friendService.SaveRequests(authUser) if err != nil { log.Printf("Unable to add user as friend: %v\n%v", memberId, err) e := apperrors.NewBadRequest(apperrors.UnableAddError) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Emit friends request to the added user h.socketService.EmitAddFriendRequest(memberId, &model.FriendRequest{ Id: authUser.ID, Username: authUser.Username, Image: authUser.Image, Type: model.Incoming, }) } c.JSON(http.StatusOK, true) } // RemoveFriend removes the given member param from the current // users friends. // RemoveFriend godoc // @Tags Friends // @Summary Remove Friend // @Produce json // @Param memberId path string true "User ID" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /account/{memberId}/friend [delete] func (h *Handler) RemoveFriend(c *gin.Context) { userId := c.MustGet("userId").(string) memberId := c.Param("memberId") if userId == memberId { e := apperrors.NewBadRequest(apperrors.RemoveYourselfError) c.JSON(e.Status(), gin.H{ "error": e, }) return } authUser, err := h.friendService.GetMemberById(userId) if err != nil { log.Printf("Unable to find user: %v\n%v", memberId, err) e := apperrors.NewNotFound("user", memberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } member, err := h.friendService.GetMemberById(memberId) if err != nil { log.Printf("Unable to find user: %v\n%v", memberId, err) e := apperrors.NewNotFound("user", memberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if isFriend(authUser, member.ID) { err = h.friendService.RemoveFriend(member.ID, authUser.ID) if err != nil { log.Printf("Unable to remove user from friends: %v\n%v", memberId, err) e := apperrors.NewBadRequest(apperrors.UnableRemoveError) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Emit signal to remove the person from the friends h.socketService.EmitRemoveFriend(userId, memberId) } c.JSON(http.StatusOK, true) } // AcceptFriendRequest accepts the friend request from the given member param // AcceptFriendRequest godoc // @Tags Friends // @Summary Accept Friend's Request // @Produce json // @Param memberId path string true "User ID" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /account/{memberId}/friend/accept [post] func (h *Handler) AcceptFriendRequest(c *gin.Context) { userId := c.MustGet("userId").(string) memberId := c.Param("memberId") if userId == memberId { e := apperrors.NewBadRequest(apperrors.AcceptYourselfError) c.JSON(e.Status(), gin.H{ "error": e, }) return } authUser, err := h.friendService.GetMemberById(userId) if err != nil { log.Printf("Unable to find user: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } member, err := h.friendService.GetMemberById(memberId) if err != nil { log.Printf("Unable to find user: %v\n%v", memberId, err) e := apperrors.NewNotFound("user", memberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the current user is in the members requests if containsRequest(member, authUser) { // Add each other to friends authUser.Friends = append(authUser.Friends, *member) member.Friends = append(member.Friends, *authUser) err = h.friendService.SaveRequests(member) if err != nil { log.Printf("Unable to accept friends request from user: %v\n%v", memberId, err) e := apperrors.NewBadRequest(apperrors.UnableAcceptError) c.JSON(e.Status(), gin.H{ "error": e, }) return } err = h.friendService.SaveRequests(authUser) if err != nil { log.Printf("Unable to accept friends request from user: %v\n%v", memberId, err) e := apperrors.NewBadRequest(apperrors.UnableAcceptError) c.JSON(e.Status(), gin.H{ "error": e, }) return } err = h.friendService.DeleteRequest(authUser.ID, member.ID) if err != nil { log.Printf("Unable to remove user from friends: %v\n%v", memberId, err) e := apperrors.NewBadRequest(apperrors.UnableRemoveError) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Emit friend information to the accepted person h.socketService.EmitAddFriend(authUser, member) } c.JSON(http.StatusOK, true) } // CancelFriendRequest removes the given member param from the current // users requests. // CancelFriendRequest godoc // @Tags Friends // @Summary Cancel Friend's Request // @Produce json // @Param memberId path string true "User ID" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /account/{memberId}/friend/cancel [post] func (h *Handler) CancelFriendRequest(c *gin.Context) { userId := c.MustGet("userId").(string) memberId := c.Param("memberId") if userId == memberId { e := apperrors.NewBadRequest(apperrors.CancelYourselfError) c.JSON(e.Status(), gin.H{ "error": e, }) return } authUser, err := h.friendService.GetMemberById(userId) if err != nil { log.Printf("Unable to find user: %v\n%v", memberId, err) e := apperrors.NewNotFound("user", memberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } member, err := h.friendService.GetMemberById(memberId) if err != nil { log.Printf("Unable to find user: %v\n%v", memberId, err) e := apperrors.NewNotFound("user", memberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the member is in the current user's requests if containsRequest(authUser, member) || containsRequest(member, authUser) { err := h.friendService.DeleteRequest(member.ID, authUser.ID) if err != nil { log.Printf("Unable to remove user from friends: %v\n%v", memberId, err) e := apperrors.NewBadRequest(apperrors.UnableRemoveError) c.JSON(e.Status(), gin.H{ "error": e, }) return } } c.JSON(http.StatusOK, true) } // isFriend checks if the given users are friends func isFriend(user *model.User, userId string) bool { for _, v := range user.Friends { if v.ID == userId { return true } } return false } // containsRequest checks if the given user has a friends request from the current one func containsRequest(user *model.User, current *model.User) bool { for _, v := range user.Requests { if v.ID == current.ID { return true } } return false } ================================================ FILE: server/handler/friend_handler_test.go ================================================ package handler import ( "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "net/http" "net/http/httptest" "testing" ) func TestHandler_GetUserFriends(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() friends := make([]model.Friend, 0) for i := 0; i < 5; i++ { mockFriend := fixture.GetMockUser() friends = append(friends, model.Friend{ Id: mockFriend.ID, Username: mockFriend.Username, Image: mockFriend.Image, IsOnline: false, }) } t.Run("Successful Fetch", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockFriendService.On("GetFriends", authUser.ID).Return(&friends, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) request, err := http.NewRequest(http.MethodGet, "/api/account/me/friends", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(friends) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockFriendService.On("GetFriends", authUser.ID).Return(&friends, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, FriendService: mockFriendService, }) request, err := http.NewRequest(http.MethodGet, "/api/account/me/friends", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockFriendService.AssertNotCalled(t, "GetFriends", authUser.ID, "") }) t.Run("Error", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockFriendService.On("GetFriends", authUser.ID).Return(nil, fmt.Errorf("some error down call chain")) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) request, err := http.NewRequest(http.MethodGet, "/api/account/me/friends", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewNotFound("user", authUser.ID) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) } func TestHandler_GetUserRequests(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() requests := make([]model.FriendRequest, 0) for i := 0; i < 5; i++ { mockRequest := fixture.GetMockUser() requests = append(requests, model.FriendRequest{ Id: mockRequest.ID, Username: mockRequest.Username, Image: mockRequest.Image, Type: 0, }) } t.Run("Successful Fetch", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockFriendService.On("GetRequests", authUser.ID).Return(&requests, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) request, err := http.NewRequest(http.MethodGet, "/api/account/me/pending", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(requests) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockFriendService.On("GetRequests", authUser.ID).Return(&requests, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, FriendService: mockFriendService, }) request, err := http.NewRequest(http.MethodGet, "/api/account/me/pending", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockFriendService.AssertNotCalled(t, "GetRequests", authUser.ID, "") }) t.Run("Error", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockFriendService.On("GetRequests", authUser.ID).Return(nil, fmt.Errorf("some error down call chain")) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) request, err := http.NewRequest(http.MethodGet, "/api/account/me/pending", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewNotFound("user", authUser.ID) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) } func TestHandler_AcceptFriendRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) current := fixture.GetMockUser() t.Run("Successfully accepted request", func(t *testing.T) { mockUser := fixture.GetMockUser() mockUser.Requests = append(mockUser.Requests, *current) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockFriendService.On("SaveRequests", current). Run(func(args mock.Arguments) { current.Friends = append(current.Friends, *mockUser) }). Return(nil) mockFriendService.On("SaveRequests", mockUser). Run(func(args mock.Arguments) { mockUser.Friends = append(mockUser.Friends, *current) }). Return(nil) mockFriendService.On("DeleteRequest", current.ID, mockUser.ID).Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitAddFriend", current, mockUser).Return() // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend/accept", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) t.Run("Member does not contain a request", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend/accept", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "SaveRequests") mockSocketService.AssertNotCalled(t, "EmitAddFriendRequest") }) t.Run("Unauthorized", func(t *testing.T) { id := fixture.RandID() mockUserService := new(mocks.UserService) mockUserService.On("GetMemberById", id).Return(nil, nil) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, UserService: mockUserService, }) url := fmt.Sprintf("/api/account/%s/friend/accept", id) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockUserService.AssertNotCalled(t, "GetMemberById", id) }) t.Run("NotFound", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockError := apperrors.NewNotFound("user", mockUser.ID) mockFriendService.On("GetMemberById", mockUser.ID).Return(nil, mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend/accept", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "SaveRequests") mockSocketService.AssertNotCalled(t, "EmitAddFriendRequest") }) t.Run("MemberId and UserId are the same", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend/accept", current.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.AcceptYourselfError) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertNotCalled(t, "GetMemberById") mockFriendService.AssertNotCalled(t, "SaveRequests") mockFriendService.AssertNotCalled(t, "DeleteRequest") mockSocketService.AssertNotCalled(t, "EmitAddFriend") }) t.Run("Error", func(t *testing.T) { mockUser := fixture.GetMockUser() mockUser.Requests = append(mockUser.Requests, *current) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockError := apperrors.NewBadRequest(apperrors.UnableAcceptError) mockFriendService.On("SaveRequests", mockUser). Return(mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend/accept", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitAddFriend") }) } func TestHandler_SendFriendRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) current := fixture.GetMockUser() t.Run("Successfully send request", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockFriendService.On("SaveRequests", current). Run(func(args mock.Arguments) { current.Requests = append(current.Requests, *mockUser) }). Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitAddFriendRequest", mockUser.ID, &model.FriendRequest{ Id: current.ID, Username: current.Username, Image: current.Image, Type: 1, }).Return() // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) t.Run("Member already is a friend", func(t *testing.T) { mockUser := fixture.GetMockUser() current.Friends = append(current.Friends, *mockUser) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "SaveRequests") mockSocketService.AssertNotCalled(t, "EmitAddFriendRequest") }) t.Run("Member already contains request", func(t *testing.T) { mockUser := fixture.GetMockUser() current.Requests = append(current.Requests, *mockUser) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "SaveRequests") mockSocketService.AssertNotCalled(t, "EmitAddFriendRequest") }) t.Run("Unauthorized", func(t *testing.T) { id := fixture.RandID() mockUserService := new(mocks.UserService) mockUserService.On("GetMemberById", id).Return(nil, nil) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, UserService: mockUserService, }) url := fmt.Sprintf("/api/account/%s/friend", id) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockUserService.AssertNotCalled(t, "GetMemberById", id) }) t.Run("NotFound", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockError := apperrors.NewNotFound("user", mockUser.ID) mockFriendService.On("GetMemberById", mockUser.ID).Return(nil, mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "SaveRequests") mockSocketService.AssertNotCalled(t, "EmitAddFriendRequest") }) t.Run("MemberId and UserId are the same", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", current.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.AddYourselfError) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertNotCalled(t, "GetMemberById") mockFriendService.AssertNotCalled(t, "SaveRequests") mockSocketService.AssertNotCalled(t, "EmitAddFriendRequest") }) t.Run("Error", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockError := apperrors.NewBadRequest(apperrors.UnableAddError) mockFriendService.On("SaveRequests", current). Return(mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitAddFriendRequest") }) } func TestHandler_RemoveFriend(t *testing.T) { // Setup gin.SetMode(gin.TestMode) current := fixture.GetMockUser() t.Run("Successfully removed friend", func(t *testing.T) { mockUser := fixture.GetMockUser() current.Friends = append(current.Friends, *mockUser) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockFriendService.On("RemoveFriend", mockUser.ID, current.ID). Run(func(args mock.Arguments) { current.Friends = make([]model.User, 0) }). Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitRemoveFriend", current.ID, mockUser.ID).Return() // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) t.Run("Member was not a friend", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "RemoveFriend") mockSocketService.AssertNotCalled(t, "EmitRemoveFriend") }) t.Run("Unauthorized", func(t *testing.T) { id := fixture.RandID() mockUserService := new(mocks.UserService) mockUserService.On("GetMemberById", id).Return(nil, nil) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, UserService: mockUserService, }) url := fmt.Sprintf("/api/account/%s/friend", id) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockUserService.AssertNotCalled(t, "GetMemberById", id) }) t.Run("NotFound", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockError := apperrors.NewNotFound("user", mockUser.ID) mockFriendService.On("GetMemberById", mockUser.ID).Return(nil, mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "RemoveFriend") mockSocketService.AssertNotCalled(t, "EmitRemoveFriend") }) t.Run("MemberId and UserId are the same", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", current.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.RemoveYourselfError) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertNotCalled(t, "GetMemberById") mockFriendService.AssertNotCalled(t, "RemoveFriend") mockSocketService.AssertNotCalled(t, "EmitRemoveFriend") }) t.Run("Error", func(t *testing.T) { mockUser := fixture.GetMockUser() current.Friends = append(current.Friends, *mockUser) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockError := apperrors.NewBadRequest(apperrors.UnableRemoveError) mockFriendService.On("RemoveFriend", mockUser.ID, current.ID). Return(mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, SocketService: mockSocketService, }) url := fmt.Sprintf("/api/account/%s/friend", mockUser.ID) request, err := http.NewRequest(http.MethodDelete, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitRemoveFriend") }) } func TestHandler_CancelFriendRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) current := fixture.GetMockUser() t.Run("Successfully canceled request", func(t *testing.T) { mockUser := fixture.GetMockUser() current.Requests = append(current.Requests, *mockUser) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockFriendService.On("DeleteRequest", mockUser.ID, current.ID). Run(func(args mock.Arguments) { current.Requests = make([]model.User, 0) }). Return(nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) url := fmt.Sprintf("/api/account/%s/friend/cancel", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) t.Run("Current does not contain a request", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) url := fmt.Sprintf("/api/account/%s/friend/cancel", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "DeleteRequest") }) t.Run("Unauthorized", func(t *testing.T) { id := fixture.RandID() mockUserService := new(mocks.UserService) mockUserService.On("GetMemberById", id).Return(nil, nil) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, UserService: mockUserService, }) url := fmt.Sprintf("/api/account/%s/friend/cancel", id) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockUserService.AssertNotCalled(t, "GetMemberById", id) }) t.Run("NotFound", func(t *testing.T) { mockUser := fixture.GetMockUser() mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockError := apperrors.NewNotFound("user", mockUser.ID) mockFriendService.On("GetMemberById", mockUser.ID).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) url := fmt.Sprintf("/api/account/%s/friend/cancel", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) mockFriendService.AssertNotCalled(t, "DeleteRequest") }) t.Run("MemberId and UserId are the same", func(t *testing.T) { mockFriendService := new(mocks.FriendService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) url := fmt.Sprintf("/api/account/%s/friend/cancel", current.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.CancelYourselfError) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertNotCalled(t, "GetMemberById") mockFriendService.AssertNotCalled(t, "DeleteRequest") }) t.Run("Error", func(t *testing.T) { mockUser := fixture.GetMockUser() current.Requests = append(current.Requests, *mockUser) mockFriendService := new(mocks.FriendService) mockFriendService.On("GetMemberById", current.ID).Return(current, nil) mockFriendService.On("GetMemberById", mockUser.ID).Return(mockUser, nil) mockError := apperrors.NewBadRequest(apperrors.UnableRemoveError) mockFriendService.On("DeleteRequest", mockUser.ID, current.ID). Return(mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(current.ID) NewHandler(&Config{ R: router, FriendService: mockFriendService, }) url := fmt.Sprintf("/api/account/%s/friend/cancel", mockUser.ID) request, err := http.NewRequest(http.MethodPost, url, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockFriendService.AssertExpectations(t) }) } ================================================ FILE: server/handler/guild_handler.go ================================================ package handler import ( "context" "fmt" "github.com/gin-gonic/gin" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/lib/pq" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "log" "mime/multipart" "net/http" "os" "strconv" "strings" ) /* * GuildHandler contains all routes related to guild actions (/api/guilds) */ // GetUserGuilds returns the current users guilds // GetUserGuilds godoc // @Tags Guilds // @Summary Get Current User's Guilds // @Produce json // @Success 200 {array} model.GuildResponse // @Failure 404 {object} model.ErrorResponse // @Router /guilds [get] func (h *Handler) GetUserGuilds(c *gin.Context) { userId := c.MustGet("userId").(string) guilds, err := h.guildService.GetUserGuilds(userId) if err != nil { log.Printf("Unable to find guilds for id: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, guilds) } // GetGuildMembers returns the given guild's members // GetGuildMembers godoc // @Tags Guilds // @Summary Get Guild Members // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {array} model.MemberResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /guilds/{guildId}/members [get] func (h *Handler) GetGuildMembers(c *gin.Context) { userId := c.MustGet("userId").(string) guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { log.Printf("Unable to find guilds for id: %v\n%v", guildId, err) e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if a member if !isMember(guild, userId) { e := apperrors.NewAuthorization(apperrors.NotAMember) c.JSON(e.Status(), gin.H{ "error": e, }) return } members, err := h.guildService.GetGuildMembers(userId, guildId) if err != nil { log.Printf("Unable to find guilds for id: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, members) } // GetVCMembers returns the given guild's members that are currently in the VC // GetVCMembers godoc // @Tags Guilds // @Summary Get Guild VC Members // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {array} model.VCMemberResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /guilds/{guildId}/vcmembers [get] func (h *Handler) GetVCMembers(c *gin.Context) { userId := c.MustGet("userId").(string) guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { log.Printf("Unable to find guilds for id: %v\n%v", guildId, err) e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if a member if !isMember(guild, userId) { e := apperrors.NewAuthorization(apperrors.NotAMember) c.JSON(e.Status(), gin.H{ "error": e, }) return } members, err := h.guildService.GetVCMembers(guild.ID) if err != nil { log.Printf("Unable to find vc members for id: %v\n%v", guildId, err) e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, members) } type createGuildRequest struct { // Guild Name. 3 to 30 characters Name string `json:"name"` } //@name CreateGuildRequest func (r createGuildRequest) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Name, validation.Required, validation.Length(3, 30)), ) } func (r *createGuildRequest) sanitize() { r.Name = strings.TrimSpace(r.Name) } // CreateGuild creates a guild // CreateGuild godoc // @Tags Guilds // @Summary Create Guild // @Accepts json // @Produce json // @Param request body createGuildRequest true "Create Guild" // @Success 201 {array} model.GuildResponse // @Failure 400 {object} model.ErrorsResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/create [post] func (h *Handler) CreateGuild(c *gin.Context) { var req createGuildRequest // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } req.sanitize() userId := c.MustGet("userId").(string) authUser, err := h.guildService.GetUser(userId) if err != nil { log.Printf("Unable to find user: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the user is already in 100 guilds if len(authUser.Guilds) >= model.MaximumGuilds { e := apperrors.NewBadRequest(apperrors.GuildLimitReached) c.JSON(e.Status(), gin.H{ "error": e, }) return } guildParams := model.Guild{ Name: req.Name, OwnerId: userId, } // Add the current user as a member guildParams.Members = append(guildParams.Members, *authUser) guild, err := h.guildService.CreateGuild(&guildParams) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Create the default 'general' channel for the guild channelParams := model.Channel{ GuildID: &guild.ID, Name: "general", IsPublic: true, } channel, err := h.channelService.CreateChannel(&channelParams) if err != nil { log.Printf("Failed to create channel for guild: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } c.JSON(http.StatusCreated, guild.SerializeGuild(channel.ID)) } // editGuildRequest specifies the form to edit the guild. // If Image is not nil then the guild's icon got changed. // If Icon is not nil then the guild kept its old one. // If both are nil then the icon got reset. type editGuildRequest struct { // Guild Name. 3 to 30 characters Name string `form:"name"` // image/png or image/jpeg Image *multipart.FileHeader `form:"image" swaggertype:"string" format:"binary"` // The old guild icon url if no new image is selected. Set to null to reset the guild icon Icon *string `form:"icon"` } //@name EditGuildRequest func (r editGuildRequest) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Name, validation.Required, validation.Length(3, 30)), ) } func (r *editGuildRequest) sanitize() { r.Name = strings.TrimSpace(r.Name) } // EditGuild edits the given guild // EditGuild godoc // @Tags Guilds // @Summary Edit Guild // @Accepts mpfd // @Produce json // @Param request body editGuildRequest true "Edit Guild" // @Param guildId path string true "Guild ID" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId} [put] func (h *Handler) EditGuild(c *gin.Context) { var req editGuildRequest // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } req.sanitize() userId := c.MustGet("userId").(string) guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } guild.Name = req.Name // Guild icon got changed if req.Image != nil { // Validate image mime-type is allowable mimeType := req.Image.Header.Get("Content-Type") if valid := isAllowedImageType(mimeType); !valid { toFieldErrorResponse(c, "Image", apperrors.InvalidImageType) return } directory := fmt.Sprintf("valkyrie/guilds/%s", guild.ID) url, err := h.userService.ChangeAvatar(req.Image, directory) if err != nil { e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.Icon != nil { _ = h.userService.DeleteImage(*guild.Icon) } guild.Icon = &url // Guild kept its old icon } else if req.Icon != nil { guild.Icon = req.Icon // Guild reset its icon } else { guild.Icon = nil } if err = h.guildService.UpdateGuild(guild); err != nil { log.Printf("Failed to update guild: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit guild changes to guild members h.socketService.EmitEditGuild(guild) c.JSON(http.StatusOK, true) } // GetInvite creates an invitation for the given guild // The isPermanent query parameter specifies if the invite // should not be deleted after it got used // GetInvite godoc // @Tags Guilds // @Summary Get Guild Invite // @Produce json // @Param guildId path string true "Guild ID" // @Param isPermanent query boolean false "Is Permanent" // @Success 200 string link // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId}/invite [get] func (h *Handler) GetInvite(c *gin.Context) { guildId := c.Param("guildId") permanent := c.Query("isPermanent") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } userId := c.MustGet("userId").(string) // Must be a member to create an invitation if !isMember(guild, userId) { e := apperrors.NewAuthorization(apperrors.MustBeMemberInvite) c.JSON(e.Status(), gin.H{ "error": e, }) return } isPermanent := false if permanent != "" { isPermanent, err = strconv.ParseBool(permanent) if err != nil { e := apperrors.NewBadRequest(apperrors.IsPermanentError) c.JSON(e.Status(), gin.H{ "error": e, }) return } } ctx := context.Background() link, err := h.guildService.GenerateInviteLink(ctx, guild.ID, isPermanent) if err != nil { e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } if isPermanent { guild.InviteLinks = append(guild.InviteLinks, link) _ = h.guildService.UpdateGuild(guild) } origin := os.Getenv("CORS_ORIGIN") c.JSON(http.StatusOK, fmt.Sprintf("%s/%s", origin, link)) } // DeleteGuildInvites removes all permanent invites from the given guild // DeleteGuildInvites godoc // @Tags Guilds // @Summary Delete all permanent invite links // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {object} model.Success // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId}/invite [delete] func (h *Handler) DeleteGuildInvites(c *gin.Context) { userId := c.MustGet("userId").(string) guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.InvalidateInvitesError) c.JSON(e.Status(), gin.H{ "error": e, }) return } ctx := context.Background() h.guildService.InvalidateInvites(ctx, guild) guild.InviteLinks = make(pq.StringArray, 0) if err = h.guildService.UpdateGuild(guild); err != nil { log.Printf("Failed to delete guild invites: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } c.JSON(http.StatusOK, true) } type joinReq struct { Link string `json:"link"` } //@name JoinRequest func (r joinReq) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Link, validation.Required), ) } func (r *joinReq) sanitize() { r.Link = strings.TrimSpace(r.Link) } // JoinGuild adds the current user to invited guild // JoinGuild godoc // @Tags Guilds // @Summary Join Guild // @Produce json // @Param request body joinReq true "Join Guild" // @Success 200 {object} model.GuildResponse // @Failure 400 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/join [post] func (h *Handler) JoinGuild(c *gin.Context) { var req joinReq // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } req.sanitize() userId := c.MustGet("userId").(string) authUser, err := h.guildService.GetUser(userId) if err != nil { log.Printf("Unable to find user: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the user has reached the guild limit if len(authUser.Guilds) >= model.MaximumGuilds { e := apperrors.NewBadRequest(apperrors.GuildLimitReached) c.JSON(e.Status(), gin.H{ "error": e, }) return } // If the link contains the domain, remove it if strings.Contains(req.Link, "/") { req.Link = req.Link[strings.LastIndex(req.Link, "/")+1:] } ctx := context.Background() guildId, err := h.guildService.GetGuildIdFromInvite(ctx, req.Link) if err != nil { e := apperrors.NewBadRequest(apperrors.InvalidInviteError) c.JSON(e.Status(), gin.H{ "error": e, }) return } guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewBadRequest(apperrors.InvalidInviteError) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the user is banned from the guild if isBanned(guild, authUser.ID) { e := apperrors.NewBadRequest(apperrors.BannedFromServer) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the user is already a member if isMember(guild, authUser.ID) { e := apperrors.NewBadRequest(apperrors.AlreadyMember) c.JSON(e.Status(), gin.H{ "error": e, }) return } guild.Members = append(guild.Members, *authUser) if err = h.guildService.UpdateGuild(guild); err != nil { log.Printf("Failed to join guild: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit new member to the guild h.socketService.EmitAddMember(guild.ID, authUser) channel, _ := h.guildService.GetDefaultChannel(guildId) c.JSON(http.StatusCreated, guild.SerializeGuild(channel.ID)) } // LeaveGuild leaves the given guild // LeaveGuild godoc // @Tags Guilds // @Summary Leave Guild // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {object} model.Success // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId} [delete] func (h *Handler) LeaveGuild(c *gin.Context) { userId := c.MustGet("userId").(string) guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.OwnerId == userId { e := apperrors.NewAuthorization(apperrors.OwnerCantLeave) c.JSON(e.Status(), gin.H{ "error": e, }) return } if err := h.guildService.RemoveMember(userId, guildId); err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit signal to remove the member from the guild h.socketService.EmitRemoveMember(guild.ID, userId) c.JSON(http.StatusOK, true) } // DeleteGuild deletes the given guild // DeleteGuild godoc // @Tags Guilds // @Summary Delete Guild // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {object} model.Success // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId}/delete [delete] func (h *Handler) DeleteGuild(c *gin.Context) { userId := c.MustGet("userId").(string) guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.DeleteGuildError) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Get the ID of all members to emit the deletion to members := make([]string, 0) for _, member := range guild.Members { members = append(members, member.ID) } if err := h.guildService.DeleteGuild(guildId); err != nil { log.Printf("Failed to leave guild: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit signal to remove the guild to its members h.socketService.EmitDeleteGuild(guildId, members) c.JSON(http.StatusOK, true) } // isMember checks if the given user is a member of the guild func isMember(guild *model.Guild, userId string) bool { for _, v := range guild.Members { if v.ID == userId { return true } } return false } // isBanned checks if the given user is banned from the guild func isBanned(guild *model.Guild, userId string) bool { for _, v := range guild.Bans { if v.ID == userId { return true } } return false } ================================================ FILE: server/handler/guild_handler_test.go ================================================ package handler import ( "bytes" "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "net/http" "net/http/httptest" "net/url" "os" "strings" "testing" ) func TestHandler_GetUserGuilds(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() response := make([]model.GuildResponse, 0) for i := 0; i < 5; i++ { mockGuild := fixture.GetMockGuild("") response = append(response, mockGuild.SerializeGuild(fixture.RandID())) } t.Run("Successful Fetch", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUserGuilds", authUser.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) request, err := http.NewRequest(http.MethodGet, "/api/guilds", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(response) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockFriendService := new(mocks.FriendService) mockFriendService.On("GetUserGuilds", authUser.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, FriendService: mockFriendService, }) request, err := http.NewRequest(http.MethodGet, "/api/guilds", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockFriendService.AssertNotCalled(t, "GetUserGuilds", authUser.ID, "") }) t.Run("Error", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUserGuilds", authUser.ID).Return(nil, fmt.Errorf("some error down call chain")) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) request, err := http.NewRequest(http.MethodGet, "/api/guilds", nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewNotFound("user", authUser.ID) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) } func TestHandler_GetGuildMembers(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() guild := fixture.GetMockGuild("") guild.Members = append(guild.Members, *authUser) response := make([]model.MemberResponse, 0) for i := 0; i < 5; i++ { mockUser := fixture.GetMockUser() response = append(response, model.MemberResponse{ Id: mockUser.ID, Username: mockUser.Username, Image: mockUser.Image, IsOnline: mockUser.IsOnline, CreatedAt: mockUser.CreatedAt, UpdatedAt: mockUser.UpdatedAt, IsFriend: false, }) } t.Run("Successful Fetch", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", guild.ID).Return(guild, nil) mockGuildService.On("GetGuildMembers", authUser.ID, guild.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/members", guild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(response) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", guild.ID).Return(guild, nil) mockGuildService.On("GetGuildMembers", authUser.ID, guild.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/members", guild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockGuildService.AssertNotCalled(t, "GetGuild", guild.ID) mockGuildService.AssertNotCalled(t, "GetGuildMembers", authUser.ID, guild.ID) }) t.Run("Not a member of the guild", func(t *testing.T) { invalidGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", invalidGuild.ID).Return(invalidGuild, nil) mockGuildService.On("GetGuildMembers", authUser.ID, invalidGuild.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/members", invalidGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) mockError := apperrors.NewAuthorization(apperrors.NotAMember) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", invalidGuild.ID) mockGuildService.AssertNotCalled(t, "GetGuildMembers", authUser.ID, invalidGuild.ID) }) t.Run("Guild not found", func(t *testing.T) { invalidGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", invalidGuild.ID) mockGuildService.On("GetGuild", invalidGuild.ID).Return(nil, mockError) mockGuildService.On("GetGuildMembers", authUser.ID, invalidGuild.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/members", invalidGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", invalidGuild.ID) mockGuildService.AssertNotCalled(t, "GetGuildMembers", authUser.ID, invalidGuild.ID) }) t.Run("Error", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", guild.ID).Return(guild, nil) mockError := apperrors.NewNotFound("user", authUser.ID) mockGuildService.On("GetGuildMembers", authUser.ID, guild.ID).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/members", guild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", guild.ID) mockGuildService.AssertCalled(t, "GetGuildMembers", authUser.ID, guild.ID) }) } func TestHandler_GetVCMembers(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() guild := fixture.GetMockGuild("") guild.Members = append(guild.Members, *authUser) response := make([]model.VCMemberResponse, 0) for i := 0; i < 5; i++ { mockUser := fixture.GetMockUser() guild.VCMembers = append(guild.VCMembers, *authUser) response = append(response, model.VCMemberResponse{ Id: mockUser.ID, Username: mockUser.Username, Image: mockUser.Image, IsMuted: false, IsDeafened: false, }) } t.Run("Successful Fetch", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", guild.ID).Return(guild, nil) mockGuildService.On("GetVCMembers", guild.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/vcmembers", guild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(response) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", guild.ID).Return(guild, nil) mockGuildService.On("GetVCMembers", guild.ID).Return(response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/vcmembers", guild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockGuildService.AssertNotCalled(t, "GetGuild", guild.ID) mockGuildService.AssertNotCalled(t, "GetVCMembers", guild.ID) }) t.Run("Not a member of the guild", func(t *testing.T) { invalidGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", invalidGuild.ID).Return(invalidGuild, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/vcmembers", invalidGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) mockError := apperrors.NewAuthorization(apperrors.NotAMember) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", invalidGuild.ID) mockGuildService.AssertNotCalled(t, "GetVCMembers", guild.ID) }) t.Run("Guild not found", func(t *testing.T) { invalidGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", invalidGuild.ID) mockGuildService.On("GetGuild", invalidGuild.ID).Return(nil, mockError) mockGuildService.On("GetVCMembers", guild.ID).Return(&response, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/vcmembers", invalidGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", invalidGuild.ID) mockGuildService.AssertNotCalled(t, "GetVCMembers", guild.ID) }) t.Run("Error", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", guild.ID).Return(guild, nil) mockError := apperrors.NewNotFound("guild", guild.ID) mockGuildService.On("GetVCMembers", guild.ID).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/vcmembers", guild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", guild.ID) mockGuildService.AssertCalled(t, "GetVCMembers", guild.ID) }) } func TestHandler_CreateGuild(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful guild creation", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) router := getAuthenticatedTestRouter(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) reqBody, err := json.Marshal(gin.H{ "name": mockGuild.Name, }) assert.NoError(t, err) guildParams := &model.Guild{ Name: mockGuild.Name, OwnerId: authUser.ID, } guildParams.Members = append(guildParams.Members, *authUser) mockGuildService.On("CreateGuild", guildParams).Return(mockGuild, nil) defaultChannel := fixture.GetMockChannel(mockGuild.ID) defaultChannel.Name = "general" mockGuild.Channels = append(mockGuild.Channels, *defaultChannel) mockChannelService := new(mocks.ChannelService) channelParams := &model.Channel{ GuildID: &mockGuild.ID, Name: defaultChannel.Name, IsPublic: true, } mockChannelService.On("CreateChannel", channelParams).Return(defaultChannel, nil) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(mockGuild.SerializeGuild(defaultChannel.ID)) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetUser", authUser.ID) mockGuildService.AssertCalled(t, "CreateGuild", guildParams) mockChannelService.AssertCalled(t, "CreateChannel", channelParams) }) t.Run("Error Returned from GuildService.CreateGuild", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) router := getAuthenticatedTestRouter(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) reqBody, err := json.Marshal(gin.H{ "name": mockGuild.Name, }) assert.NoError(t, err) guildParams := &model.Guild{ Name: mockGuild.Name, OwnerId: authUser.ID, } guildParams.Members = append(guildParams.Members, *authUser) mockError := apperrors.NewInternal() mockGuildService.On("CreateGuild", guildParams).Return(nil, mockError) mockChannelService := new(mocks.ChannelService) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetUser", authUser.ID) mockGuildService.AssertCalled(t, "CreateGuild", guildParams) mockChannelService.AssertNotCalled(t, "CreateChannel") }) t.Run("User already is in the maximum number of guilds", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) for i := 0; i < model.MaximumGuilds; i++ { guild := fixture.GetMockGuild("") authUser.Guilds = append(authUser.Guilds, *guild) } router := getAuthenticatedTestRouter(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) reqBody, err := json.Marshal(gin.H{ "name": mockGuild.Name, }) assert.NoError(t, err) mockError := apperrors.NewBadRequest(apperrors.GuildLimitReached) mockChannelService := new(mocks.ChannelService) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetUser", authUser.ID) mockGuildService.AssertNotCalled(t, "CreateGuild") mockChannelService.AssertNotCalled(t, "CreateChannel") }) t.Run("Unauthorized", func(t *testing.T) { router := getTestRouter() mockGuildService := new(mocks.GuildService) mockChannelService := new(mocks.ChannelService) reqBody, err := json.Marshal(gin.H{ "name": fixture.RandStringRunes(6), }) assert.NoError(t, err) NewHandler(&Config{ R: router, GuildService: mockGuildService, ChannelService: mockChannelService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "CreateGuild") mockChannelService.AssertNotCalled(t, "CreateChannel") }) } func TestHandler_CreateGuild_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() router := getAuthenticatedTestRouter(mockUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", mockUser.ID).Return(mockUser, nil) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) testCases := []struct { name string body gin.H }{ { name: "Name required", body: gin.H{}, }, { name: "Name too short", body: gin.H{ "name": fixture.RandStr(2), }, }, { name: "Name too long", body: gin.H{ "name": fixture.RandStr(31), }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() reqBody, err := json.Marshal(tc.body) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/create", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "CreateGuild") }) } } func TestHandler_UpdateGuild(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully updated guild", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) router := getAuthenticatedTestRouter(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) name := fixture.RandStringRunes(8) form := url.Values{} form.Add("name", name) mockGuildService.On("UpdateGuild", mockGuild). Run(func(args mock.Arguments) { mockGuild.Name = name }). Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitEditGuild", mockGuild).Return() NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() request, err := http.NewRequest(http.MethodPut, "/api/guilds/"+mockGuild.ID, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertCalled(t, "UpdateGuild", mockGuild) mockSocketService.AssertCalled(t, "EmitEditGuild", mockGuild) }) t.Run("Error Returned from GuildService.UpdateGuild", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) router := getAuthenticatedTestRouter(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) name := fixture.RandStringRunes(8) form := url.Values{} form.Add("name", name) mockError := apperrors.NewInternal() mockGuildService.On("UpdateGuild", mockGuild).Return(mockError) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitEditGuild", mockGuild).Return() NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() request, err := http.NewRequest(http.MethodPut, "/api/guilds/"+mockGuild.ID, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertCalled(t, "UpdateGuild", mockGuild) mockSocketService.AssertNotCalled(t, "EmitEditGuild", mockGuild) }) t.Run("Not the owner of the guild", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") router := getAuthenticatedTestRouter(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) name := fixture.RandStringRunes(8) form := url.Values{} form.Add("name", name) mockSocketService := new(mocks.SocketService) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() request, err := http.NewRequest(http.MethodPut, "/api/guilds/"+mockGuild.ID, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form mockError := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UpdateGuild", mockGuild) mockSocketService.AssertNotCalled(t, "EmitEditGuild", mockGuild) }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") router := getTestRouter() mockGuildService := new(mocks.GuildService) name := fixture.RandStringRunes(8) form := url.Values{} form.Add("name", name) mockSocketService := new(mocks.SocketService) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() request, err := http.NewRequest(http.MethodPut, "/api/guilds/"+mockGuild.ID, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UpdateGuild", mockGuild) mockSocketService.AssertNotCalled(t, "EmitEditGuild", mockGuild) }) t.Run("Guild not found", func(t *testing.T) { id := fixture.RandID() router := getAuthenticatedTestRouter(authUser.ID) mockError := apperrors.NewNotFound("guild", id) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", id).Return(nil, mockError) name := fixture.RandStringRunes(8) form := url.Values{} form.Add("name", name) mockSocketService := new(mocks.SocketService) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() request, err := http.NewRequest(http.MethodPut, "/api/guilds/"+id, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", id) mockGuildService.AssertNotCalled(t, "UpdateGuild", mock.AnythingOfType("*model.Guild")) mockSocketService.AssertNotCalled(t, "EmitEditGuild", mock.AnythingOfType("*model.Guild")) }) } func TestHandler_UpdateGuild_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() router := getAuthenticatedTestRouter(mockUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", mockUser.ID).Return(mockUser, nil) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) testCases := []struct { name string body url.Values }{ { name: "Name required", body: map[string][]string{}, }, { name: "Name too short", body: map[string][]string{ "name": {fixture.RandStr(2)}, }, }, { name: "Name too long", body: map[string][]string{ "name": {fixture.RandStr(31)}, }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() reqUrl := fmt.Sprintf("/api/guilds/%s", fixture.RandID()) form := tc.body request, _ := http.NewRequest(http.MethodPut, reqUrl, strings.NewReader(form.Encode())) request.Form = form router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "GetGuild") }) } } func TestHandler_GetInvite(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() origin := "http://localhost:3000" err := os.Setenv("CORS_ORIGIN", origin) assert.NoError(t, err) t.Run("Successful Fetch", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *authUser) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) getInviteArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockGuild.ID, false, } link := fixture.RandID() mockGuildService.On("GenerateInviteLink", getInviteArgs...).Return(link, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(fmt.Sprintf("%s/%s", origin, link)) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) }) t.Run("Not a member of the guild", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.MustBeMemberInvite) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "GenerateInviteLink") }) t.Run("Guild not found", func(t *testing.T) { id := fixture.RandID() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", id) mockGuildService.On("GetGuild", id).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", id) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", id) mockGuildService.AssertNotCalled(t, "GenerateInviteLink") }) t.Run("Invalid isPermanent value", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *authUser) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite?isPermanent=yes", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.IsPermanentError) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "GenerateInviteLink") }) t.Run("Invite isPermanent success", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *authUser) link := fixture.RandID() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("UpdateGuild", mockGuild). Run(func(args mock.Arguments) { mockGuild.InviteLinks = append(mockGuild.InviteLinks, link) }). Return(nil) getInviteArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockGuild.ID, true, } mockGuildService.On("GenerateInviteLink", getInviteArgs...).Return(link, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite?isPermanent=true", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(fmt.Sprintf("%s/%s", origin, link)) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *authUser) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) getInviteArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockGuild.ID, false, } mockError := apperrors.NewInternal() mockGuildService.On("GenerateInviteLink", getInviteArgs...).Return("", mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) } func TestHandler_DeleteGuildInvites(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully deleted", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockGuild, } mockGuildService.On("InvalidateInvites", mockArgs...).Return() mockGuildService.On("UpdateGuild", mockGuild).Return(nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), mockGuild, } mockGuildService.On("InvalidateInvites", mockArgs...).Return() mockError := apperrors.NewInternal() mockGuildService.On("UpdateGuild", mockGuild).Return(mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Not the server owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidateInvitesError) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "InvalidateInvites") mockGuildService.AssertNotCalled(t, "UpdateGuild") }) t.Run("Guild not found", func(t *testing.T) { id := fixture.RandID() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", id) mockGuildService.On("GetGuild", id).Return(nil, mockError) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/invite", id) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", id) mockGuildService.AssertNotCalled(t, "InvalidateInvites") mockGuildService.AssertNotCalled(t, "UpdateGuild") }) } func TestHandler_JoinGuild(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully joined", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") link := fixture.RandID() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) mockArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), link, } mockGuildService.On("GetGuildIdFromInvite", mockArgs...).Return(mockGuild.ID, nil) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("UpdateGuild", mockGuild).Return(nil) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildService.On("GetDefaultChannel", mockGuild.ID).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitAddMember", mockGuild.ID, authUser).Return() rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "link": link, }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(mockGuild.SerializeGuild(mockChannel.ID)) assert.NoError(t, err) assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Link required", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "link": "", }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "GetGuildIdFromInvite") mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockGuildService.AssertNotCalled(t, "GetDefaultChannel") mockSocketService.AssertNotCalled(t, "EmitAddMember") }) t.Run("Unauthorized", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "link": "link", }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "GetGuildIdFromInvite") mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockGuildService.AssertNotCalled(t, "GetDefaultChannel") mockSocketService.AssertNotCalled(t, "EmitAddMember") }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") link := fixture.RandID() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) mockArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), link, } mockGuildService.On("GetGuildIdFromInvite", mockArgs...).Return(mockGuild.ID, nil) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewInternal() mockGuildService.On("UpdateGuild", mockGuild).Return(mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "link": link, }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockGuildService.AssertNotCalled(t, "GetDefaultChannel") mockSocketService.AssertNotCalled(t, "EmitAddMember") }) t.Run("User is already in the maximum number of guilds", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() newAuthUser := fixture.GetMockUser() for i := 0; i < model.MaximumGuilds; i++ { mockGuild := fixture.GetMockGuild("") newAuthUser.Guilds = append(newAuthUser.Guilds, *mockGuild) } mockGuildService.On("GetUser", newAuthUser.ID).Return(newAuthUser, nil) router := getAuthenticatedTestRouter(newAuthUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "link": "link", }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.GuildLimitReached) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetUser", newAuthUser.ID) mockGuildService.AssertNotCalled(t, "GetGuildIdFromInvite") mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockGuildService.AssertNotCalled(t, "GetDefaultChannel") mockSocketService.AssertNotCalled(t, "EmitAddMember") }) t.Run("User is banned from the guild", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Bans = append(mockGuild.Bans, *authUser) link := fixture.RandID() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) mockArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), link, } mockGuildService.On("GetGuildIdFromInvite", mockArgs...).Return(mockGuild.ID, nil) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "link": link, }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.BannedFromServer) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockGuildService.AssertNotCalled(t, "GetDefaultChannel") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockSocketService.AssertNotCalled(t, "EmitAddMember") }) t.Run("Invalid Invite", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") link := fixture.RandID() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) mockArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), link, } mockError := apperrors.NewBadRequest(apperrors.InvalidInviteError) mockGuildService.On("GetGuildIdFromInvite", mockArgs...).Return(mockGuild.ID, mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "link": link, }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "GetDefaultChannel") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockSocketService.AssertNotCalled(t, "EmitAddMember") }) t.Run("Already a member", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *authUser) link := fixture.RandID() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) mockArgs := mock.Arguments{ mock.AnythingOfType("*context.emptyCtx"), link, } mockGuildService.On("GetGuildIdFromInvite", mockArgs...).Return(mockGuild.ID, nil) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "link": link, }) assert.NoError(t, err) request, err := http.NewRequest(http.MethodPost, "/api/guilds/join", bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.AlreadyMember) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockGuildService.AssertNotCalled(t, "GetDefaultChannel") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockSocketService.AssertNotCalled(t, "EmitAddMember") }) } func TestHandler_LeaveGuild(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully left the guild", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("RemoveMember", authUser.ID, mockGuild.ID).Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitRemoveMember", mockGuild.ID, authUser.ID).Return() // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("AuthUser is the owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.OwnerCantLeave) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") }) t.Run("Guild not found", func(t *testing.T) { id := fixture.RandID() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", id) mockGuildService.On("GetGuild", id).Return(nil, mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s", id) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", id) mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") }) t.Run("Server Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewInternal() mockGuildService.On("RemoveMember", authUser.ID, mockGuild.ID).Return(mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitRemoveMember") }) } func TestHandler_DeleteGuild(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully deleted", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("DeleteGuild", mockGuild.ID).Return(nil) mockSocketService := new(mocks.SocketService) members := make([]string, 0) mockSocketService.On("EmitDeleteGuild", mockGuild.ID, members).Return() // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/delete", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Not the guild owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/delete", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.DeleteGuildError) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "DeleteGuild") mockSocketService.AssertNotCalled(t, "EmitDeleteGuild") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/delete", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "DeleteGuild") mockSocketService.AssertNotCalled(t, "EmitDeleteGuild") }) t.Run("Guild not found", func(t *testing.T) { id := fixture.RandID() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", id) mockGuildService.On("GetGuild", id).Return(nil, mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/delete", id) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", id) mockGuildService.AssertNotCalled(t, "DeleteGuild") mockSocketService.AssertNotCalled(t, "EmitDeleteGuild") }) t.Run("Server Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewInternal() mockGuildService.On("DeleteGuild", mockGuild.ID).Return(mockError) mockSocketService := new(mocks.SocketService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/delete", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitDeleteGuild") }) } ================================================ FILE: server/handler/handler.go ================================================ package handler import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/contrib/static" "github.com/gin-gonic/gin" "log" "net/http" // Register swagger docs _ "github.com/sentrionic/valkyrie/docs" "github.com/sentrionic/valkyrie/handler/middleware" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/swaggo/files" // swagger embed files "github.com/swaggo/gin-swagger" // gin-swagger middleware "time" ) // Handler struct holds required services for handler to function type Handler struct { userService model.UserService friendService model.FriendService guildService model.GuildService channelService model.ChannelService messageService model.MessageService socketService model.SocketService MaxBodyBytes int64 } // Config will hold services that will eventually be injected into this // handler layer on handler initialization type Config struct { R *gin.Engine UserService model.UserService FriendService model.FriendService GuildService model.GuildService ChannelService model.ChannelService MessageService model.MessageService SocketService model.SocketService TimeoutDuration time.Duration MaxBodyBytes int64 } // NewHandler initializes the handler with required injected services along with http routes // Does not return as it deals directly with a reference to the gin Engine func NewHandler(c *Config) { // Create a handler (which will later have injected services) h := &Handler{ userService: c.UserService, friendService: c.FriendService, guildService: c.GuildService, channelService: c.ChannelService, messageService: c.MessageService, socketService: c.SocketService, MaxBodyBytes: c.MaxBodyBytes, } c.R.NoRoute(func(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{ "error": "No route found. Go to https://api.valkyrieapp.xyz/swagger/index.html for a list of all routes", }) }) c.R.Use(static.Serve("/", static.LocalFile("./static", true))) if gin.Mode() != gin.TestMode { c.R.Use(middleware.Timeout(c.TimeoutDuration, apperrors.NewServiceUnavailable())) } c.R.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) // Create an account group ag := c.R.Group("api/account") ag.POST("/register", h.Register) ag.POST("/login", h.Login) ag.POST("/logout", h.Logout) ag.POST("/forgot-password", h.ForgotPassword) ag.POST("/reset-password", h.ResetPassword) ag.Use(middleware.AuthUser()) ag.GET("", h.GetCurrent) ag.PUT("", h.Edit) ag.PUT("/change-password", h.ChangePassword) ag.GET("/me/friends", h.GetUserFriends) ag.GET("/me/pending", h.GetUserRequests) ag.POST("/:memberId/friend", h.SendFriendRequest) ag.DELETE("/:memberId/friend", h.RemoveFriend) ag.POST("/:memberId/friend/accept", h.AcceptFriendRequest) ag.POST("/:memberId/friend/cancel", h.CancelFriendRequest) // Create a guild group gg := c.R.Group("api/guilds") gg.Use(middleware.AuthUser()) gg.GET("/:guildId/members", h.GetGuildMembers) gg.GET("/:guildId/vcmembers", h.GetVCMembers) gg.GET("", h.GetUserGuilds) gg.POST("/create", h.CreateGuild) gg.GET("/:guildId/invite", h.GetInvite) gg.DELETE("/:guildId/invite", h.DeleteGuildInvites) gg.POST("/join", h.JoinGuild) gg.GET("/:guildId/member", h.GetMemberSettings) gg.PUT("/:guildId/member", h.EditMemberSettings) gg.DELETE("/:guildId", h.LeaveGuild) gg.PUT("/:guildId", h.EditGuild) gg.DELETE("/:guildId/delete", h.DeleteGuild) gg.GET("/:guildId/bans", h.GetBanList) gg.POST("/:guildId/bans", h.BanMember) gg.DELETE("/:guildId/bans", h.UnbanMember) gg.POST("/:guildId/kick", h.KickMember) // Create a channels group cg := c.R.Group("api/channels") cg.Use(middleware.AuthUser()) // Route parameters cause conflicts so they have to use the same parameter name cg.GET("/:id", h.GuildChannels) // id -> guildId cg.POST("/:id", h.CreateChannel) // id -> guildId cg.GET("/:id/members", h.PrivateChannelMembers) // id -> channelId cg.POST("/:id/dm", h.GetOrCreateDM) // id -> memberId cg.GET("/me/dm", h.DirectMessages) // cg.PUT("/:id", h.EditChannel) // id -> channelId cg.DELETE("/:id", h.DeleteChannel) // id -> channelId cg.DELETE("/:id/dm", h.CloseDM) // id -> channelId // Create a messages group mg := c.R.Group("api/messages") mg.Use(middleware.AuthUser()) mg.GET("/:channelId", h.GetMessages) mg.POST("/:channelId", h.CreateMessage) mg.PUT("/:messageId", h.EditMessage) mg.DELETE("/:messageId", h.DeleteMessage) } // setUserSession saves the users ID in the session func setUserSession(c *gin.Context, id string) { session := sessions.Default(c) session.Set("userId", id) if err := session.Save(); err != nil { log.Printf("error setting the session: %v\n", err.Error()) } } func toFieldErrorResponse(c *gin.Context, field, message string) { c.JSON(http.StatusBadRequest, gin.H{ "errors": []model.FieldError{ { Field: field, Message: message, }, }, }) } ================================================ FILE: server/handler/member_handler.go ================================================ package handler import ( "github.com/gin-gonic/gin" validation "github.com/go-ozzo/ozzo-validation/v4" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "log" "net/http" "strings" ) /* * MemberHandler contains all routes related to mod actions (/api/guilds) */ // memberReq contains the MemberId of the user // that needs to be moderated type memberReq struct { MemberId string `json:"memberId"` } //@name MemberRequest func (r memberReq) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.MemberId, validation.Required, is.UTFDigit), ) } // GetMemberSettings gets the current user's role color and nickname // for the given guild // GetMemberSettings godoc // @Tags Members // @Summary Get Member Settings // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {object} model.MemberSettings // @Failure 404 {object} model.ErrorResponse // @Router /guilds/{guildId}/member [get] func (h *Handler) GetMemberSettings(c *gin.Context) { guildId := c.Param("guildId") userId := c.MustGet("userId").(string) _, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } settings, err := h.guildService.GetMemberSettings(userId, guildId) if err != nil { log.Printf("Unable to find settings: %v\n%v", userId, err) e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, settings) } type memberSettingsReq struct { Nickname *string `json:"nickname"` Color *string `json:"color"` } //@name MemberSettingsRequest func (r memberSettingsReq) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Nickname, validation.NilOrNotEmpty, validation.Length(3, 30)), validation.Field(&r.Color, validation.NilOrNotEmpty, is.HexColor), ) } func (r *memberSettingsReq) sanitize() { if r.Nickname != nil { nickname := strings.TrimSpace(*r.Nickname) r.Nickname = &nickname } } // EditMemberSettings changes the current user's role color and nickname // for the given guild // EditMemberSettings godoc // @Tags Members // @Summary Edit Member Settings // @Accepts json // @Produce json // @Param request body memberSettingsReq true "Edit Member" // @Param guildId path string true "Guild ID" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorsResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId}/member [put] func (h *Handler) EditMemberSettings(c *gin.Context) { var req memberSettingsReq if ok := bindData(c, &req); !ok { return } req.sanitize() guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } userId := c.MustGet("userId").(string) // Check if the user is a member of the guild if !isMember(guild, userId) { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } settings := &model.MemberSettings{ Nickname: req.Nickname, Color: req.Color, } err = h.guildService.UpdateMemberSettings(settings, userId, guildId) if err != nil { log.Printf("Unable to update settings for user: %v\n%v", userId, err) e := apperrors.NewInternal() c.JSON(e.Status(), gin.H{ "error": e, }) return } c.JSON(http.StatusOK, true) } // GetBanList returns a list of all banned users for the given guild // GetBanList godoc // @Tags Members // @Summary Get Guild Ban list // @Produce json // @Param guildId path string true "Guild ID" // @Success 200 {array} model.BanResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId}/bans [get] func (h *Handler) GetBanList(c *gin.Context) { guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } userId := c.MustGet("userId").(string) if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } bans, err := h.guildService.GetBanList(guildId) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // If the guild does not have any bans, return an empty array if len(*bans) == 0 { empty := make([]model.BanResponse, 0) c.JSON(http.StatusOK, empty) return } c.JSON(http.StatusOK, bans) } // BanMember bans the provided member from the given guild // BanMember godoc // @Tags Members // @Summary Ban Member // @Produce json // @Param guildId path string true "Guild ID" // @Param request body memberReq true "Member ID" // @Success 200 {array} model.Success // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId}/bans [post] func (h *Handler) BanMember(c *gin.Context) { var req memberReq if ok := bindData(c, &req); !ok { return } guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } userId := c.MustGet("userId").(string) if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } member, err := h.guildService.GetUser(req.MemberId) if err != nil { log.Printf("Unable to find user: %v\n%v", req.MemberId, err) e := apperrors.NewNotFound("user", req.MemberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if member.ID == userId { e := apperrors.NewBadRequest(apperrors.BanYourselfError) c.JSON(e.Status(), gin.H{ "error": e, }) return } guild.Bans = append(guild.Bans, *member) if err = h.guildService.UpdateGuild(guild); err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } err = h.guildService.RemoveMember(req.MemberId, guildId) if err != nil { log.Printf("Failed to ban member: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit signals to remove the member from the guild h.socketService.EmitRemoveMember(guild.ID, member.ID) h.socketService.EmitRemoveFromGuild(member.ID, guildId) c.JSON(http.StatusOK, true) } // UnbanMember unbans the specified user from the given guild // BanMember godoc // @Tags Members // @Summary Unban Member // @Produce json // @Param guildId path string true "Guild ID" // @Param request body memberReq true "Member ID" // @Success 200 {array} model.Success // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId}/bans [delete] func (h *Handler) UnbanMember(c *gin.Context) { var req memberReq if ok := bindData(c, &req); !ok { return } guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } userId := c.MustGet("userId").(string) if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } if req.MemberId == userId { e := apperrors.NewBadRequest(apperrors.UnbanYourselfError) c.JSON(e.Status(), gin.H{ "error": e, }) return } if err := h.guildService.UnbanMember(req.MemberId, guildId); err != nil { log.Printf("Failed to unban member: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } c.JSON(http.StatusOK, true) } // KickMember kicks the provided member from the given guild // KickMember godoc // @Tags Members // @Summary Kick Member // @Produce json // @Param guildId path string true "Guild ID" // @Param request body memberReq true "Member ID" // @Success 200 {array} model.Success // @Failure 400 {object} model.ErrorResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /guilds/{guildId}/kick [post] func (h *Handler) KickMember(c *gin.Context) { var req memberReq if ok := bindData(c, &req); !ok { return } guildId := c.Param("guildId") guild, err := h.guildService.GetGuild(guildId) if err != nil { e := apperrors.NewNotFound("guild", guildId) c.JSON(e.Status(), gin.H{ "error": e, }) return } userId := c.MustGet("userId").(string) if guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.MustBeOwner) c.JSON(e.Status(), gin.H{ "error": e, }) return } member, err := h.guildService.GetUser(req.MemberId) if err != nil { log.Printf("Unable to find user: %v\n%v", req.MemberId, err) e := apperrors.NewNotFound("user", req.MemberId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if member.ID == userId { e := apperrors.NewBadRequest(apperrors.KickYourselfError) c.JSON(e.Status(), gin.H{ "error": e, }) return } err = h.guildService.RemoveMember(req.MemberId, guildId) if err != nil { log.Printf("Failed to kick member: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit signals to remove the member from the guild h.socketService.EmitRemoveMember(guild.ID, member.ID) h.socketService.EmitRemoveFromGuild(member.ID, guildId) c.JSON(http.StatusOK, true) } ================================================ FILE: server/handler/member_handler_test.go ================================================ package handler import ( "bytes" "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "net/http" "net/http/httptest" "testing" ) func TestHandler_GetMemberSettings(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() mockGuild := fixture.GetMockGuild("") settings := &model.MemberSettings{ Nickname: nil, Color: nil, } t.Run("Successfully fetched settings", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockArgs := mock.Arguments{ authUser.ID, mockGuild.ID, } mockGuildService.On("GetMemberSettings", mockArgs...).Return(settings, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(settings) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Unauthorized", func(t *testing.T) { mockGuildService := new(mocks.GuildService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "GetMemberSettings") }) t.Run("Guild not found", func(t *testing.T) { id := fixture.RandID() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", id) mockGuildService.On("GetGuild", id).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/member", id) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", id) mockGuildService.AssertNotCalled(t, "GetMemberSettings") }) t.Run("Server Error", func(t *testing.T) { mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewNotFound("user", authUser.ID) mockArgs := mock.Arguments{ authUser.ID, mockGuild.ID, } mockGuildService.On("GetMemberSettings", mockArgs...).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) } func TestHandler_EditMemberSettings(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() nickname := fixture.Username() color := "#fff" settings := &model.MemberSettings{ Nickname: &nickname, Color: &color, } t.Run("Successfully edited settings", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *authUser) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockArgs := mock.Arguments{ settings, authUser.ID, mockGuild.ID, } mockGuildService.On("UpdateMemberSettings", mockArgs...).Return(nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "nickname": nickname, "color": color, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) request, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Successfully reset member settings", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *authUser) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockArgs := mock.Arguments{ &model.MemberSettings{}, authUser.ID, mockGuild.ID, } mockGuildService.On("UpdateMemberSettings", mockArgs...).Return(nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "nickname": nil, "color": nil, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) request, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Not a member of the server", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "nickname": nickname, "color": color, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) request, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewNotFound("guild", mockGuild.ID) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockGuildService.AssertNotCalled(t, "UpdateMemberSettings") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) request, err := http.NewRequest(http.MethodPut, reqUrl, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "UpdateMemberSettings") }) t.Run("Guild not found", func(t *testing.T) { id := fixture.RandID() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", id) mockGuildService.On("GetGuild", id).Return(nil, mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "nickname": nickname, "color": color, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/member", id) request, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", id) mockGuildService.AssertNotCalled(t, "UpdateMemberSettings") }) t.Run("Server Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *authUser) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewInternal() mockArgs := mock.Arguments{ settings, authUser.ID, mockGuild.ID, } mockGuildService.On("UpdateMemberSettings", mockArgs...).Return(mockError) // a response recorder for getting written http response rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "nickname": nickname, "color": color, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/member", mockGuild.ID) request, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) } func TestHandler_EditMemberSettings_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() router := getAuthenticatedTestRouter(mockUser.ID) mockGuildService := new(mocks.GuildService) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) testCases := []struct { name string body gin.H }{ { name: "Nickname too short", body: gin.H{ "nickname": fixture.RandStringRunes(2), }, }, { name: "Nickname too long", body: gin.H{ "nickname": fixture.RandStringRunes(32), }, }, { name: "Color not a hex color", body: gin.H{ "color": fixture.RandStringRunes(6), }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() reqBody, err := json.Marshal(tc.body) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/member", fixture.RandID()) request, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "UpdateMemberSettings") }) } } func TestHandler_GetBanList(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful Fetch", func(t *testing.T) { response := make([]model.BanResponse, 0) mockGuild := fixture.GetMockGuild(authUser.ID) for i := 0; i < 5; i++ { mockUser := fixture.GetMockUser() response = append(response, model.BanResponse{ Id: mockUser.ID, Username: mockUser.Username, Image: mockUser.Image, }) } mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("GetBanList", mockGuild.ID).Return(&response, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(response) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Not the owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockGuildService.AssertNotCalled(t, "GetBanList") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuildService := new(mocks.GuildService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "GetBanList") }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewInternal() mockGuildService.On("GetBanList", mockGuild.ID).Return(nil, mockError) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodGet, reqUrl, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) } func TestHandler_BanMember(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful Ban", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("GetUser", mockMember.ID).Return(mockMember, nil) mockGuildService.On("UpdateGuild", mockGuild).Return(nil) args := mock.Arguments{ mockMember.ID, mockGuild.ID, } mockGuildService.On("RemoveMember", args...).Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitRemoveMember", mockGuild.ID, mockMember.ID) mockSocketService.On("EmitRemoveFromGuild", mockMember.ID, mockGuild.ID) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Not the owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("GetUser", mockMember.ID).Return(mockMember, nil) mockGuildService.On("UpdateGuild", mockGuild).Return(nil) mockError := apperrors.NewInternal() args := mock.Arguments{ mockMember.ID, mockGuild.ID, } mockGuildService.On("RemoveMember", args...).Return(mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("Guild not found", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", mockGuild.ID) mockGuildService.On("GetGuild", mockGuild.ID).Return(nil, mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("Member not found", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewNotFound("user", mockMember.ID) mockGuildService.On("GetUser", mockMember.ID).Return(nil, mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertCalled(t, "GetUser", mockMember.ID) mockGuildService.AssertNotCalled(t, "UpdateGuild") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("MemberId required", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{}) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "UpdateGuild") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("MemberId and AuthUserId are equal", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": authUser.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.BanYourselfError) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertCalled(t, "GetUser", authUser.ID) mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) } func TestHandler_KickMember(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful Kick", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("GetUser", mockMember.ID).Return(mockMember, nil) args := mock.Arguments{ mockMember.ID, mockGuild.ID, } mockGuildService.On("RemoveMember", args...).Return(nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitRemoveMember", mockGuild.ID, mockMember.ID) mockSocketService.On("EmitRemoveFromGuild", mockMember.ID, mockGuild.ID) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Not the owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("GetUser", mockMember.ID).Return(mockMember, nil) mockError := apperrors.NewInternal() args := mock.Arguments{ mockMember.ID, mockGuild.ID, } mockGuildService.On("RemoveMember", args...).Return(mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("Guild not found", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", mockGuild.ID) mockGuildService.On("GetGuild", mockGuild.ID).Return(nil, mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("Member not found", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewNotFound("user", mockMember.ID) mockGuildService.On("GetUser", mockMember.ID).Return(nil, mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertCalled(t, "GetUser", mockMember.ID) mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("MemberId required", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{}) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "GetUser") mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) t.Run("MemberId and AuthUserId are equal", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockGuildService.On("GetUser", authUser.ID).Return(authUser, nil) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": authUser.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/kick", mockGuild.ID) request, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.KickYourselfError) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertCalled(t, "GetUser", authUser.ID) mockGuildService.AssertNotCalled(t, "RemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveMember") mockSocketService.AssertNotCalled(t, "EmitRemoveFromGuild") }) } func TestHandler_UnbanMember(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful Unban", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) args := mock.Arguments{ mockMember.ID, mockGuild.ID, } mockGuildService.On("UnbanMember", args...).Return(nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Not the owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.MustBeOwner) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UnbanMember") }) t.Run("Unauthorized", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "UnbanMember") }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockError := apperrors.NewInternal() args := mock.Arguments{ mockMember.ID, mockGuild.ID, } mockGuildService.On("UnbanMember", args...).Return(mockError) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, SocketService: mockSocketService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertExpectations(t) }) t.Run("Guild not found", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockMember := fixture.GetMockUser() mockGuildService := new(mocks.GuildService) mockError := apperrors.NewNotFound("guild", mockGuild.ID) mockGuildService.On("GetGuild", mockGuild.ID).Return(nil, mockError) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "memberId": mockMember.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UnbanMember") }) t.Run("MemberId required", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{}) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockGuildService.AssertNotCalled(t, "GetGuild") mockGuildService.AssertNotCalled(t, "UnbanMember") }) t.Run("MemberId and AuthUserId are equal", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, GuildService: mockGuildService, }) reqBody, err := json.Marshal(gin.H{ "memberId": authUser.ID, }) assert.NoError(t, err) reqUrl := fmt.Sprintf("/api/guilds/%s/bans", mockGuild.ID) request, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") router.ServeHTTP(rr, request) mockError := apperrors.NewBadRequest(apperrors.UnbanYourselfError) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockGuildService.AssertNotCalled(t, "UnbanMember") }) } ================================================ FILE: server/handler/message_handler.go ================================================ package handler import ( "fmt" "github.com/gin-gonic/gin" validation "github.com/go-ozzo/ozzo-validation/v4" gonanoid "github.com/matoous/go-nanoid" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "log" "mime/multipart" "net/http" "strings" "time" ) /* * MessageHandler contains all routes related to message actions (/api/messages) */ // GetMessages returns messages for the given channel // It returns the most recent 35 or the ones after the given cursor // GetMessages godoc // @Tags Messages // @Summary Get Channel Messages // @Produce json // @Param channelId path string true "Channel ID" // @Param cursor query string false "Cursor Pagination using the createdAt field" // @Success 200 {array} model.MessageResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Router /messages/{channelId} [get] func (h *Handler) GetMessages(c *gin.Context) { channelId := c.Param("channelId") userId := c.MustGet("userId").(string) channel, err := h.channelService.Get(channelId) if err != nil { e := apperrors.NewNotFound("channel", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the user has access to said channel err = h.channelService.IsChannelMember(channel, userId) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Cursor is based on the created_at field of the message cursor := c.Query("cursor") messages, err := h.messageService.GetMessages(userId, channel, cursor) if err != nil { e := apperrors.NewNotFound("messages", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // If the channel does not have any messages, return an empty array if len(*messages) == 0 { var empty = make([]model.MessageResponse, 0) c.JSON(http.StatusOK, empty) return } c.JSON(http.StatusOK, messages) } // messageRequest contains all field required to create a message. // Either text or file must be provided type messageRequest struct { // Maximum 2000 characters Text *string `form:"text"` // image/* or audio/* File *multipart.FileHeader `form:"file" swaggertype:"string" format:"binary"` } //@name MessageRequest func (r messageRequest) validate() error { return validation.ValidateStruct(&r, validation.Field(&r.Text, validation.NilOrNotEmpty, validation.Required.When(r.File == nil). Error(apperrors.MessageOrFileRequired), validation.Length(1, 2000), ), ) } func (r *messageRequest) sanitize() { if r.Text != nil { text := strings.TrimSpace(*r.Text) r.Text = &text } } // CreateMessage creates a message in the given channel // CreateMessage godoc // @Tags Messages // @Summary Create Messages // @Accepts mpfd // @Produce json // @Param channelId path string true "Channel ID" // @Param request body messageRequest true "Create Message" // @Success 201 {object} model.Success // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /messages/{channelId} [post] func (h *Handler) CreateMessage(c *gin.Context) { channelId := c.Param("channelId") userId := c.MustGet("userId").(string) var req messageRequest // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } req.sanitize() channel, err := h.channelService.Get(channelId) if err != nil { e := apperrors.NewNotFound("channel", channelId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if the user has access to said channel err = h.channelService.IsChannelMember(channel, userId) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } author, err := h.userService.Get(userId) if err != nil { e := apperrors.NewNotFound("user", userId) c.JSON(e.Status(), gin.H{ "error": e, }) return } params := model.Message{ UserId: userId, ChannelId: channel.ID, } params.Text = req.Text if req.File != nil { mimeType := req.File.Header.Get("Content-Type") if valid := isAllowedFileType(mimeType); !valid { toFieldErrorResponse(c, "File", apperrors.InvalidImageType) return } // Prevent file upload on the live server. // Remove the if part if you do want upload var attachment *model.Attachment if gin.Mode() == gin.ReleaseMode { id, _ := gonanoid.Nanoid(20) // Random image to test files in the app attachment = &model.Attachment{ ID: id, Url: fmt.Sprintf("https://picsum.photos/seed/%s/600", id), FileType: "image/jpeg", Filename: id, } } else { attachment, err = h.messageService.UploadFile(req.File, channel.ID) if err != nil { c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } } params.Attachment = attachment } message, err := h.messageService.CreateMessage(¶ms) if err != nil { log.Printf("Failed to create message: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } response := model.MessageResponse{ Id: message.ID, Text: message.Text, CreatedAt: message.CreatedAt, UpdatedAt: message.UpdatedAt, Attachment: message.Attachment, User: model.MemberResponse{ Id: author.ID, Username: author.Username, Image: author.Image, IsOnline: author.IsOnline, CreatedAt: author.CreatedAt, UpdatedAt: author.UpdatedAt, IsFriend: false, }, } // Get member settings if it is not a DM if !channel.IsDM { settings, _ := h.guildService.GetMemberSettings(userId, *channel.GuildID) response.User.Nickname = settings.Nickname response.User.Color = settings.Color } // Emit new message to the channel h.socketService.EmitNewMessage(channelId, &response) if channel.IsDM { // Open the DM and push it to the top _ = h.channelService.OpenDMForAll(channelId) // Post a notification h.socketService.EmitNewDMNotification(channelId, author) } else { // Update last activity in channel channel.LastActivity = time.Now() _ = h.channelService.UpdateChannel(channel) // Post a notification h.socketService.EmitNewNotification(*channel.GuildID, channelId) } c.JSON(http.StatusCreated, true) } // EditMessage edits the given message with the given text // EditMessage godoc // @Tags Messages // @Summary Edit Messages // @Accepts json // @Produce json // @Param messageId path string true "Message ID" // @Param request body messageRequest true "Edit Message" // @Success 200 {object} model.Success // @Failure 400 {object} model.ErrorsResponse // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /messages/{messageId} [put] func (h *Handler) EditMessage(c *gin.Context) { messageId := c.Param("messageId") userId := c.MustGet("userId").(string) var req messageRequest // Bind incoming json to struct and check for validation errors if ok := bindData(c, &req); !ok { return } message, err := h.messageService.Get(messageId) if err != nil { e := apperrors.NewNotFound("message", messageId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if message.UserId != userId { e := apperrors.NewAuthorization(apperrors.EditMessageError) c.JSON(e.Status(), gin.H{ "error": e, }) return } message.Text = req.Text if err = h.messageService.UpdateMessage(message); err != nil { log.Printf("Failed to edit message: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } response := model.MessageResponse{ Id: message.ID, Text: message.Text, CreatedAt: message.CreatedAt, UpdatedAt: message.UpdatedAt, Attachment: message.Attachment, User: model.MemberResponse{ Id: userId, }, } // Emit edited message to the channel h.socketService.EmitEditMessage(message.ChannelId, &response) c.JSON(http.StatusOK, true) } // DeleteMessage deletes the given message // DeleteMessage godoc // @Tags Messages // @Summary Delete Messages // @Produce json // @Param messageId path string true "Message ID" // @Success 200 {object} model.Success // @Failure 401 {object} model.ErrorResponse // @Failure 404 {object} model.ErrorResponse // @Failure 500 {object} model.ErrorResponse // @Router /messages/{messageId} [delete] func (h *Handler) DeleteMessage(c *gin.Context) { messageId := c.Param("messageId") userId := c.MustGet("userId").(string) message, err := h.messageService.Get(messageId) if err != nil { e := apperrors.NewNotFound("message", messageId) c.JSON(e.Status(), gin.H{ "error": e, }) return } channel, err := h.channelService.Get(message.ChannelId) if err != nil { e := apperrors.NewNotFound("message", messageId) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Check if message author or guild owner if !channel.IsDM { guild, err := h.guildService.GetGuild(*channel.GuildID) if err != nil { e := apperrors.NewNotFound("message", messageId) c.JSON(e.Status(), gin.H{ "error": e, }) return } if message.UserId != userId && guild.OwnerId != userId { e := apperrors.NewAuthorization(apperrors.DeleteMessageError) c.JSON(e.Status(), gin.H{ "error": e, }) return } // Only message author check required } else { if message.UserId != userId { e := apperrors.NewAuthorization(apperrors.DeleteDMMessageError) c.JSON(e.Status(), gin.H{ "error": e, }) return } } if err = h.messageService.DeleteMessage(message); err != nil { log.Printf("Failed to delete message: %v\n", err.Error()) c.JSON(apperrors.Status(err), gin.H{ "error": err, }) return } // Emit delete message to the channel h.socketService.EmitDeleteMessage(message.ChannelId, message.ID) c.JSON(http.StatusOK, true) } ================================================ FILE: server/handler/message_handler_test.go ================================================ package handler import ( "bytes" "encoding/json" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" ) func TestHandler_GetMessages(t *testing.T) { gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successful fetch", func(t *testing.T) { mockChannel := fixture.GetMockChannel(fixture.RandID()) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(nil) mockMessageService := new(mocks.MessageService) args := mock.Arguments{ authUser.ID, mockChannel, "", } response := make([]model.MessageResponse, 0) for i := 0; i < 25; i++ { message := fixture.GetMockMessageResponse("", mockChannel.ID) response = append(response, *message) } mockMessageService.On("GetMessages", args...).Return(&response, nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, }) request, err := http.NewRequest(http.MethodGet, "/api/messages/"+mockChannel.ID, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(response) assert.NoError(t, err) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) mockMessageService.AssertExpectations(t) }) t.Run("No channel found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("channel", id) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", id).Return(nil, mockError) mockMessageService := new(mocks.MessageService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, }) request, err := http.NewRequest(http.MethodGet, "/api/messages/"+id, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", id) mockChannelService.AssertNotCalled(t, "IsChannelMember") mockMessageService.AssertNotCalled(t, "GetMessages") }) t.Run("Not a member of the channel", func(t *testing.T) { mockChannel := fixture.GetMockChannel(fixture.RandID()) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockError := apperrors.NewAuthorization(apperrors.Unauthorized) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(mockError) mockMessageService := new(mocks.MessageService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, }) request, err := http.NewRequest(http.MethodGet, "/api/messages/"+mockChannel.ID, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) mockMessageService.AssertNotCalled(t, "GetMessages") }) t.Run("Unauthorized", func(t *testing.T) { id := fixture.RandID() mockChannelService := new(mocks.ChannelService) mockMessageService := new(mocks.MessageService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, }) request, err := http.NewRequest(http.MethodGet, "/api/messages/"+id, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertNotCalled(t, "Get") mockChannelService.AssertNotCalled(t, "IsChannelMember") mockMessageService.AssertNotCalled(t, "GetMessages") }) t.Run("Server Error", func(t *testing.T) { mockChannel := fixture.GetMockChannel(fixture.RandID()) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(nil) mockMessageService := new(mocks.MessageService) args := mock.Arguments{ authUser.ID, mockChannel, "", } mockError := apperrors.NewNotFound("messages", mockChannel.ID) mockMessageService.On("GetMessages", args...).Return(nil, mockError) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, }) request, err := http.NewRequest(http.MethodGet, "/api/messages/"+mockChannel.ID, nil) assert.NoError(t, err) router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) mockMessageService.AssertExpectations(t) }) } func TestHandler_CreateMessage(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully created text message", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(nil) mockUserService := new(mocks.UserService) mockUserService.On("Get", authUser.ID).Return(authUser, nil) params := model.Message{ UserId: mockMessage.UserId, ChannelId: mockMessage.ChannelId, Text: mockMessage.Text, } mockMessageService := new(mocks.MessageService) mockMessageService.On("CreateMessage", ¶ms).Return(mockMessage, nil) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetMemberSettings", authUser.ID, mockGuild.ID).Return(&model.MemberSettings{}, nil) mockSocketService := new(mocks.SocketService) response := model.MessageResponse{ Id: mockMessage.ID, Text: mockMessage.Text, CreatedAt: mockMessage.CreatedAt, UpdatedAt: mockMessage.UpdatedAt, Attachment: mockMessage.Attachment, User: model.MemberResponse{ Id: authUser.ID, Username: authUser.Username, Image: authUser.Image, IsOnline: authUser.IsOnline, CreatedAt: authUser.CreatedAt, UpdatedAt: authUser.UpdatedAt, IsFriend: false, }, } mockSocketService.On("EmitNewMessage", mockChannel.ID, &response).Return() mockChannelService.On("UpdateChannel", mockChannel).Return(nil) mockSocketService.On("EmitNewNotification", mockGuild.ID, mockChannel.ID) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, GuildService: mockGuildService, SocketService: mockSocketService, UserService: mockUserService, }) form := url.Values{} form.Add("text", *mockMessage.Text) request, err := http.NewRequest(http.MethodPost, "/api/messages/"+mockChannel.ID, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) mockMessageService.AssertExpectations(t) mockGuildService.AssertExpectations(t) mockSocketService.AssertExpectations(t) mockUserService.AssertExpectations(t) }) t.Run("Channel not found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("channel", id) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", id).Return(nil, mockError) mockUserService := new(mocks.UserService) mockMessageService := new(mocks.MessageService) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, GuildService: mockGuildService, SocketService: mockSocketService, UserService: mockUserService, }) form := url.Values{} form.Add("text", fixture.RandStringRunes(8)) request, err := http.NewRequest(http.MethodPost, "/api/messages/"+id, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", id) mockChannelService.AssertNotCalled(t, "IsChannelMember") mockUserService.AssertNotCalled(t, "Get") mockMessageService.AssertNotCalled(t, "CreateMessage") mockSocketService.AssertNotCalled(t, "EmitNewMessage") }) t.Run("Not a member of the channel", func(t *testing.T) { mockChannel := fixture.GetMockChannel("") mockError := apperrors.NewAuthorization(apperrors.Unauthorized) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(mockError) mockUserService := new(mocks.UserService) mockMessageService := new(mocks.MessageService) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, GuildService: mockGuildService, SocketService: mockSocketService, UserService: mockUserService, }) form := url.Values{} form.Add("text", fixture.RandStringRunes(8)) request, err := http.NewRequest(http.MethodPost, "/api/messages/"+mockChannel.ID, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockChannelService.AssertCalled(t, "IsChannelMember", mockChannel, authUser.ID) mockUserService.AssertNotCalled(t, "Get") mockMessageService.AssertNotCalled(t, "CreateMessage") mockSocketService.AssertNotCalled(t, "EmitNewMessage") }) t.Run("Unauthorized", func(t *testing.T) { id := fixture.RandID() mockChannelService := new(mocks.ChannelService) mockUserService := new(mocks.UserService) mockMessageService := new(mocks.MessageService) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getTestRouter() NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, GuildService: mockGuildService, SocketService: mockSocketService, UserService: mockUserService, }) form := url.Values{} form.Add("text", fixture.RandStringRunes(8)) request, err := http.NewRequest(http.MethodPost, "/api/messages/"+id, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form router.ServeHTTP(rr, request) mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertNotCalled(t, "Get") mockChannelService.AssertNotCalled(t, "IsChannelMember") mockUserService.AssertNotCalled(t, "Get") mockMessageService.AssertNotCalled(t, "CreateMessage") mockSocketService.AssertNotCalled(t, "EmitNewMessage") }) t.Run("Text Message Creation failure", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(nil) mockUserService := new(mocks.UserService) mockUserService.On("Get", authUser.ID).Return(authUser, nil) params := model.Message{ UserId: mockMessage.UserId, ChannelId: mockMessage.ChannelId, Text: mockMessage.Text, } mockError := apperrors.NewInternal() mockMessageService := new(mocks.MessageService) mockMessageService.On("CreateMessage", ¶ms).Return(nil, mockError) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, GuildService: mockGuildService, SocketService: mockSocketService, UserService: mockUserService, }) form := url.Values{} form.Add("text", *mockMessage.Text) request, err := http.NewRequest(http.MethodPost, "/api/messages/"+mockChannel.ID, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form router.ServeHTTP(rr, request) respBody, err := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockChannelService.AssertCalled(t, "IsChannelMember", mockChannel, authUser.ID) mockMessageService.AssertCalled(t, "CreateMessage", ¶ms) mockUserService.AssertCalled(t, "Get", authUser.ID) mockChannelService.AssertNotCalled(t, "UpdateChannel") mockSocketService.AssertNotCalled(t, "EmitNewMessage") }) t.Run("Disallowed mimetype", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(nil) mockUserService := new(mocks.UserService) mockUserService.On("Get", authUser.ID).Return(authUser, nil) mockMessageService := new(mocks.MessageService) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, GuildService: mockGuildService, SocketService: mockSocketService, UserService: mockUserService, }) multipartImageFixture := fixture.NewMultipartImage("image.txt", "image/txt") defer multipartImageFixture.Close() request, err := http.NewRequest(http.MethodPost, "/api/messages/"+mockChannel.ID, multipartImageFixture.MultipartBody) assert.NoError(t, err) request.Header.Set("Content-Type", multipartImageFixture.ContentType) router.ServeHTTP(rr, request) assert.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rr.Code) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockChannelService.AssertCalled(t, "IsChannelMember", mockChannel, authUser.ID) mockUserService.AssertCalled(t, "Get", authUser.ID) mockMessageService.AssertNotCalled(t, "UploadFile") mockMessageService.AssertNotCalled(t, "CreateMessage") mockChannelService.AssertNotCalled(t, "UpdateChannel") mockSocketService.AssertNotCalled(t, "EmitNewMessage") }) t.Run("Image Message Creation Success", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID) mockMessage.Text = nil uploadImageFixture := fixture.NewMultipartImage("image.png", "image/png") defer uploadImageFixture.Close() formFile := uploadImageFixture.GetFormFile() attachment := &model.Attachment{ ID: fixture.RandID(), CreatedAt: time.Now(), UpdatedAt: time.Now(), Url: fixture.RandStringRunes(8), FileType: "image/png", Filename: fixture.RandStringRunes(8), MessageId: mockMessage.ID, } mockMessage.Attachment = attachment mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(nil) mockUserService := new(mocks.UserService) mockUserService.On("Get", authUser.ID).Return(authUser, nil) params := model.Message{ UserId: mockMessage.UserId, ChannelId: mockMessage.ChannelId, Attachment: attachment, } mockMessageService := new(mocks.MessageService) mockMessageService.On("UploadFile", formFile, mockChannel.ID).Return(attachment, nil) mockMessageService.On("CreateMessage", ¶ms).Return(mockMessage, nil) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetMemberSettings", authUser.ID, mockGuild.ID).Return(&model.MemberSettings{}, nil) mockSocketService := new(mocks.SocketService) response := model.MessageResponse{ Id: mockMessage.ID, Text: nil, CreatedAt: mockMessage.CreatedAt, UpdatedAt: mockMessage.UpdatedAt, Attachment: attachment, User: model.MemberResponse{ Id: authUser.ID, Username: authUser.Username, Image: authUser.Image, IsOnline: authUser.IsOnline, CreatedAt: authUser.CreatedAt, UpdatedAt: authUser.UpdatedAt, IsFriend: false, }, } mockSocketService.On("EmitNewMessage", mockChannel.ID, &response).Return() mockChannelService.On("UpdateChannel", mockChannel).Return(nil) mockSocketService.On("EmitNewNotification", mockGuild.ID, mockChannel.ID) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, GuildService: mockGuildService, SocketService: mockSocketService, UserService: mockUserService, }) multipartImageFixture := fixture.NewMultipartImage("image.png", "image/png") defer multipartImageFixture.Close() request, err := http.NewRequest(http.MethodPost, "/api/messages/"+mockChannel.ID, multipartImageFixture.MultipartBody) assert.NoError(t, err) request.Header.Set("Content-Type", multipartImageFixture.ContentType) router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) mockMessageService.AssertExpectations(t) mockGuildService.AssertExpectations(t) mockSocketService.AssertExpectations(t) mockUserService.AssertExpectations(t) }) t.Run("DM channel message success", func(t *testing.T) { mockChannel := fixture.GetMockChannel("") mockChannel.IsDM = true mockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockChannelService.On("IsChannelMember", mockChannel, authUser.ID).Return(nil) mockUserService := new(mocks.UserService) mockUserService.On("Get", authUser.ID).Return(authUser, nil) params := model.Message{ UserId: mockMessage.UserId, ChannelId: mockMessage.ChannelId, Text: mockMessage.Text, } mockMessageService := new(mocks.MessageService) mockMessageService.On("CreateMessage", ¶ms).Return(mockMessage, nil) mockSocketService := new(mocks.SocketService) response := model.MessageResponse{ Id: mockMessage.ID, Text: mockMessage.Text, CreatedAt: mockMessage.CreatedAt, UpdatedAt: mockMessage.UpdatedAt, Attachment: mockMessage.Attachment, User: model.MemberResponse{ Id: authUser.ID, Username: authUser.Username, Image: authUser.Image, IsOnline: authUser.IsOnline, CreatedAt: authUser.CreatedAt, UpdatedAt: authUser.UpdatedAt, IsFriend: false, }, } mockSocketService.On("EmitNewMessage", mockChannel.ID, &response).Return() mockSocketService.On("EmitNewDMNotification", mockChannel.ID, authUser).Return() mockChannelService.On("OpenDMForAll", mockChannel.ID).Return(nil) rr := httptest.NewRecorder() router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, ChannelService: mockChannelService, MessageService: mockMessageService, SocketService: mockSocketService, UserService: mockUserService, }) form := url.Values{} form.Add("text", *mockMessage.Text) request, err := http.NewRequest(http.MethodPost, "/api/messages/"+mockChannel.ID, strings.NewReader(form.Encode())) assert.NoError(t, err) request.Form = form router.ServeHTTP(rr, request) respBody, err := json.Marshal(true) assert.NoError(t, err) assert.Equal(t, http.StatusCreated, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockChannelService.AssertExpectations(t) mockMessageService.AssertExpectations(t) mockSocketService.AssertExpectations(t) mockUserService.AssertExpectations(t) }) } func TestHandler_CreateMessage_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() router := getAuthenticatedTestRouter(mockUser.ID) mockMessageService := new(mocks.MessageService) NewHandler(&Config{ R: router, MessageService: mockMessageService, MaxBodyBytes: 4 * 1024 * 1024, }) testCases := []struct { name string body url.Values }{ { name: "No file nor text", body: map[string][]string{}, }, { name: "Text empty and no file", body: map[string][]string{ "text": {""}, }, }, { name: "Text too long", body: map[string][]string{ "text": {fixture.RandStringRunes(2001)}, }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() form := tc.body request, _ := http.NewRequest(http.MethodPost, "/api/messages/"+fixture.RandID(), strings.NewReader(form.Encode())) request.Form = form router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockMessageService.AssertNotCalled(t, "CreateMessage") }) } } func TestHandler_UpdateMessage(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully updated", func(t *testing.T) { mockMessage := fixture.GetMockMessage(authUser.ID, "") mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockMessageService.On("UpdateMessage", mockMessage).Return(nil) response := model.MessageResponse{ Id: mockMessage.ID, Text: mockMessage.Text, CreatedAt: mockMessage.CreatedAt, UpdatedAt: mockMessage.UpdatedAt, Attachment: mockMessage.Attachment, User: model.MemberResponse{ Id: authUser.ID, }, } mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitEditMessage", mockMessage.ChannelId, &response).Return() reqBody, err := json.Marshal(gin.H{ "text": *mockMessage.Text, }) assert.NoError(t, err) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPut, "/api/messages/"+mockMessage.ID, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Message not found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("message", id) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", id).Return(nil, mockError) mockSocketService := new(mocks.SocketService) reqBody, err := json.Marshal(gin.H{ "text": fixture.RandStringRunes(12), }) assert.NoError(t, err) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPut, "/api/messages/"+id, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertCalled(t, "Get", id) mockMessageService.AssertNotCalled(t, "UpdateMessage") mockSocketService.AssertNotCalled(t, "EmitEditMessage") }) t.Run("Not the message author", func(t *testing.T) { mockMessage := fixture.GetMockMessage("", "") mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockSocketService := new(mocks.SocketService) reqBody, err := json.Marshal(gin.H{ "text": fixture.RandStringRunes(12), }) assert.NoError(t, err) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPut, "/api/messages/"+mockMessage.ID, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.EditMessageError) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertCalled(t, "Get", mockMessage.ID) mockMessageService.AssertNotCalled(t, "UpdateMessage") mockSocketService.AssertNotCalled(t, "EmitEditMessage") }) t.Run("Unauthorized", func(t *testing.T) { mockMessage := fixture.GetMockMessage("", "") mockMessageService := new(mocks.MessageService) mockSocketService := new(mocks.SocketService) reqBody, err := json.Marshal(gin.H{ "text": fixture.RandStringRunes(12), }) assert.NoError(t, err) router := getTestRouter() NewHandler(&Config{ R: router, MessageService: mockMessageService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPut, "/api/messages/"+mockMessage.ID, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertNotCalled(t, "Get") mockMessageService.AssertNotCalled(t, "UpdateMessage") mockSocketService.AssertNotCalled(t, "EmitEditMessage") }) t.Run("Server Error", func(t *testing.T) { mockMessage := fixture.GetMockMessage(authUser.ID, "") mockError := apperrors.NewInternal() mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockMessageService.On("UpdateMessage", mockMessage).Return(mockError) mockSocketService := new(mocks.SocketService) reqBody, err := json.Marshal(gin.H{ "text": *mockMessage.Text, }) assert.NoError(t, err) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodPut, "/api/messages/"+mockMessage.ID, bytes.NewBuffer(reqBody)) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) } func TestHandler_UpdateMessage_BadRequest(t *testing.T) { // Setup gin.SetMode(gin.TestMode) mockUser := fixture.GetMockUser() router := getAuthenticatedTestRouter(mockUser.ID) mockMessageService := new(mocks.MessageService) NewHandler(&Config{ R: router, MessageService: mockMessageService, MaxBodyBytes: 4 * 1024 * 1024, }) testCases := []struct { name string body url.Values }{ { name: "Text is required", body: map[string][]string{}, }, { name: "Text empty", body: map[string][]string{ "text": {""}, }, }, { name: "Text too long", body: map[string][]string{ "text": {fixture.RandStringRunes(2001)}, }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() form := tc.body request, _ := http.NewRequest(http.MethodPut, "/api/messages/"+fixture.RandID(), strings.NewReader(form.Encode())) request.Form = form router.ServeHTTP(rr, request) assert.Equal(t, http.StatusBadRequest, rr.Code) mockMessageService.AssertNotCalled(t, "UpdateMessage") }) } } func TestHandler_Delete_Message(t *testing.T) { // Setup gin.SetMode(gin.TestMode) authUser := fixture.GetMockUser() t.Run("Successfully deleted", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockMessageService.On("DeleteMessage", mockMessage).Return(nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitDeleteMessage", mockChannel.ID, mockMessage.ID) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+mockMessage.ID, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockGuildService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Message not found", func(t *testing.T) { id := fixture.RandID() mockError := apperrors.NewNotFound("message", id) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", id).Return(nil, mockError) mockChannelService := new(mocks.ChannelService) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+id, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertCalled(t, "Get", id) mockMessageService.AssertNotCalled(t, "DeleteMessage") mockChannelService.AssertNotCalled(t, "Get") mockGuildService.AssertNotCalled(t, "GetGuild") mockSocketService.AssertNotCalled(t, "EmitDeleteMessage") }) t.Run("Channel not found", func(t *testing.T) { id := fixture.RandID() mockMessage := fixture.GetMockMessage("", id) mockError := apperrors.NewNotFound("message", mockMessage.ID) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", id).Return(nil, mockError) mockGuildService := new(mocks.GuildService) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+mockMessage.ID, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertCalled(t, "Get", mockMessage.ID) mockChannelService.AssertCalled(t, "Get", id) mockMessageService.AssertNotCalled(t, "DeleteMessage") mockGuildService.AssertNotCalled(t, "GetGuild") mockSocketService.AssertNotCalled(t, "EmitDeleteMessage") }) t.Run("Delete in guild - guild owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild(authUser.ID) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockMessage := fixture.GetMockMessage("", mockChannel.ID) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockMessageService.On("DeleteMessage", mockMessage).Return(nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitDeleteMessage", mockChannel.ID, mockMessage.ID) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+mockMessage.ID, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockGuildService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Delete in guild - not the guild owner", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockMessage := fixture.GetMockMessage("", mockChannel.ID) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+mockMessage.ID, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.DeleteMessageError) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertCalled(t, "Get", mockMessage.ID) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockGuildService.AssertCalled(t, "GetGuild", mockGuild.ID) mockMessageService.AssertNotCalled(t, "DeleteMessage") mockSocketService.AssertNotCalled(t, "EmitDeleteMessage") }) t.Run("Delete in DM - Author", func(t *testing.T) { mockChannel := fixture.GetMockDMChannel() mockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockMessageService.On("DeleteMessage", mockMessage).Return(nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) mockSocketService.On("EmitDeleteMessage", mockChannel.ID, mockMessage.ID) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+mockMessage.ID, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(true) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockSocketService.AssertExpectations(t) }) t.Run("Delete in DM - Not the author", func(t *testing.T) { mockChannel := fixture.GetMockDMChannel() mockMessage := fixture.GetMockMessage("", mockChannel.ID) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockMessageService.On("DeleteMessage", mockMessage).Return(nil) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+mockMessage.ID, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.DeleteDMMessageError) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertCalled(t, "Get", mockMessage.ID) mockChannelService.AssertCalled(t, "Get", mockChannel.ID) mockMessageService.AssertNotCalled(t, "DeleteMessage") mockSocketService.AssertNotCalled(t, "EmitDeleteMessage") }) t.Run("Unauthorized", func(t *testing.T) { mockMessageService := new(mocks.MessageService) mockChannelService := new(mocks.ChannelService) mockSocketService := new(mocks.SocketService) router := getTestRouter() NewHandler(&Config{ R: router, MessageService: mockMessageService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+fixture.RandID(), nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") mockError := apperrors.NewAuthorization(apperrors.InvalidSession) respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, mockError.Status(), rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertNotCalled(t, "Get") mockChannelService.AssertNotCalled(t, "Get") mockMessageService.AssertNotCalled(t, "DeleteMessage") mockSocketService.AssertNotCalled(t, "EmitDeleteMessage") }) t.Run("Server Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID) mockMessageService := new(mocks.MessageService) mockMessageService.On("Get", mockMessage.ID).Return(mockMessage, nil) mockError := apperrors.NewInternal() mockMessageService.On("DeleteMessage", mockMessage).Return(mockError) mockChannelService := new(mocks.ChannelService) mockChannelService.On("Get", mockChannel.ID).Return(mockChannel, nil) mockGuildService := new(mocks.GuildService) mockGuildService.On("GetGuild", mockGuild.ID).Return(mockGuild, nil) mockSocketService := new(mocks.SocketService) router := getAuthenticatedTestRouter(authUser.ID) NewHandler(&Config{ R: router, MessageService: mockMessageService, GuildService: mockGuildService, ChannelService: mockChannelService, SocketService: mockSocketService, }) // a response recorder for getting written http response rr := httptest.NewRecorder() // use bytes.NewBuffer to create a reader request, err := http.NewRequest(http.MethodDelete, "/api/messages/"+mockMessage.ID, nil) assert.NoError(t, err) request.Header.Set("Content-Type", "application/json") respBody, _ := json.Marshal(gin.H{ "error": mockError, }) assert.NoError(t, err) router.ServeHTTP(rr, request) assert.Equal(t, http.StatusInternalServerError, rr.Code) assert.Equal(t, respBody, rr.Body.Bytes()) mockMessageService.AssertExpectations(t) mockChannelService.AssertExpectations(t) mockGuildService.AssertExpectations(t) mockSocketService.AssertNotCalled(t, "EmitDeleteMessage") }) } ================================================ FILE: server/handler/middleware/auth_user.go ================================================ package middleware import ( "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/model/apperrors" "log" ) // AuthUser checks if the request contains a valid session // and saves the session's userId in the context func AuthUser() gin.HandlerFunc { return func(c *gin.Context) { session := sessions.Default(c) id := session.Get("userId") if id == nil { e := apperrors.NewAuthorization(apperrors.InvalidSession) c.JSON(e.Status(), gin.H{ "error": e, }) c.Abort() return } userId := id.(string) c.Set("userId", userId) // Recreate session to extend its lifetime session.Set("userId", id) if err := session.Save(); err != nil { log.Printf("Failed recreate the session: %v\n", err.Error()) } c.Next() } } ================================================ FILE: server/handler/middleware/auth_user_test.go ================================================ package middleware import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/service" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) func TestAuthUser(t *testing.T) { gin.SetMode(gin.TestMode) uid := service.GenerateId() t.Run("Adds an userId to context", func(t *testing.T) { rr := httptest.NewRecorder() _, r := gin.CreateTestContext(rr) store := cookie.NewStore([]byte("secret")) r.Use(sessions.Sessions(model.CookieName, store)) r.Use(func(c *gin.Context) { session := sessions.Default(c) session.Set("userId", uid) }) var contextUserId string r.GET("/api/accounts", AuthUser(), func(c *gin.Context) { contextKeyVal, _ := c.Get("userId") contextUserId = contextKeyVal.(string) }) request, _ := http.NewRequest(http.MethodGet, "/api/accounts", http.NoBody) r.ServeHTTP(rr, request) assert.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, contextUserId, uid) }) t.Run("Missing Session", func(t *testing.T) { rr := httptest.NewRecorder() // creates a test context and gin engine _, r := gin.CreateTestContext(rr) store := cookie.NewStore([]byte("secret")) r.Use(sessions.Sessions(model.CookieName, store)) r.GET("/api/accounts", AuthUser()) request, _ := http.NewRequest(http.MethodGet, "/api/accounts", http.NoBody) r.ServeHTTP(rr, request) assert.Equal(t, http.StatusUnauthorized, rr.Code) }) } ================================================ FILE: server/handler/middleware/timeout.go ================================================ package middleware /* * Inspired by Golang's TimeoutHandler: https://golang.org/src/net/http/server.go?s=101514:101582#L3212 * and gin-timeout: https://github.com/vearne/gin-timeout */ import ( "bytes" "context" "encoding/json" "fmt" "github.com/sentrionic/valkyrie/model/apperrors" "net/http" "sync" "time" "github.com/gin-gonic/gin" ) // Timeout wraps the request context with a timeout func Timeout(timeout time.Duration, errTimeout *apperrors.Error) gin.HandlerFunc { return func(c *gin.Context) { // set Gin's writer as our custom writer tw := &timeoutWriter{ResponseWriter: c.Writer, h: make(http.Header)} c.Writer = tw // wrap the request context with a timeout ctx, cancel := context.WithTimeout(c.Request.Context(), timeout) defer cancel() // update gin request context c.Request = c.Request.WithContext(ctx) finished := make(chan struct{}) // to indicate handler finished panicChan := make(chan any, 1) // used to handle panics if we can't recover go func() { defer func() { if p := recover(); p != nil { panicChan <- p } }() c.Next() // calls subsequent middleware(s) and handler finished <- struct{}{} }() select { case <-panicChan: // if we cannot recover from panic, // send internal server error e := apperrors.NewInternal() tw.ResponseWriter.WriteHeader(e.Status()) eResp, _ := json.Marshal(gin.H{ "error": e, }) _, _ = tw.ResponseWriter.Write(eResp) case <-finished: // if finished, set headers and write resp tw.mu.Lock() defer tw.mu.Unlock() // map Headers from tw.Header() (written to by gin) // to tw.ResponseWriter for response dst := tw.ResponseWriter.Header() for k, vv := range tw.Header() { dst[k] = vv } tw.ResponseWriter.WriteHeader(tw.code) // tw.wbuf will have been written to already when gin writes to tw.Write() _, _ = tw.ResponseWriter.Write(tw.wbuf.Bytes()) case <-ctx.Done(): // timeout has occurred, send errTimeout and write headers tw.mu.Lock() defer tw.mu.Unlock() // ResponseWriter from gin tw.ResponseWriter.Header().Set("Content-Type", "application/json") tw.ResponseWriter.WriteHeader(errTimeout.Status()) eResp, _ := json.Marshal(gin.H{ "error": errTimeout, }) _, _ = tw.ResponseWriter.Write(eResp) c.Abort() tw.SetTimedOut() } } } // implements http.Writer, but tracks if Writer has timed out // or has already written its header to prevent // header and body overwrites // also locks access to this writer to prevent race conditions // holds the gin.ResponseWriter which we'll manually call Write() // on in the middleware function to send response type timeoutWriter struct { gin.ResponseWriter h http.Header wbuf bytes.Buffer // The zero value for Buffer is an empty buffer ready to use. mu sync.Mutex timedOut bool wroteHeader bool code int } // Writes the response, but first makes sure there // hasn't already been a timeout // In http.ResponseWriter interface func (tw *timeoutWriter) Write(b []byte) (int, error) { tw.mu.Lock() defer tw.mu.Unlock() if tw.timedOut { return 0, nil } return tw.wbuf.Write(b) } // WriteHeader In http.ResponseWriter interface func (tw *timeoutWriter) WriteHeader(code int) { checkWriteHeaderCode(code) tw.mu.Lock() defer tw.mu.Unlock() // We do not write the header if we've timed out or written the header if tw.timedOut || tw.wroteHeader { return } tw.writeHeader(code) } // set that the header has been written func (tw *timeoutWriter) writeHeader(code int) { tw.wroteHeader = true tw.code = code } // Header "relays" the header, h, set in struct // In http.ResponseWriter interface func (tw *timeoutWriter) Header() http.Header { return tw.h } // SetTimedOut sets timedOut field to true func (tw *timeoutWriter) SetTimedOut() { tw.timedOut = true } func checkWriteHeaderCode(code int) { if code < 100 || code > 999 { panic(fmt.Sprintf("invalid WriteHeader code %v", code)) } } ================================================ FILE: server/handler/mime_type.go ================================================ package handler var validImageTypes = map[string]bool{ "image/jpeg": true, "image/png": true, } // IsAllowedImageType determines if image is among types defined // in map of allowed images func isAllowedImageType(mimeType string) bool { _, exists := validImageTypes[mimeType] return exists } var validFileTypes = map[string]bool{ "image/jpeg": true, "image/png": true, "audio/mp3": true, "audio/wave": true, } // isAllowedFileType determines if the file is among types defined // in map of allowed file types func isAllowedFileType(mimeType string) bool { _, exists := validFileTypes[mimeType] return exists } ================================================ FILE: server/handler/test_helpers.go ================================================ package handler import ( "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/model" ) func getAuthenticatedTestRouter(uid string) *gin.Engine { router := gin.Default() store := cookie.NewStore([]byte("secret")) router.Use(sessions.Sessions(model.CookieName, store)) router.Use(func(c *gin.Context) { session := sessions.Default(c) session.Set("userId", uid) c.Set("userId", uid) }) return router } func getTestRouter() *gin.Engine { router := gin.Default() store := cookie.NewStore([]byte("secret")) router.Use(sessions.Sessions(model.CookieName, store)) return router } func getTestFieldErrorResponse(field, message string) gin.H { return gin.H{ "errors": []model.FieldError{ { Field: field, Message: message, }, }, } } ================================================ FILE: server/injection.go ================================================ package main import ( "fmt" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/redis" "github.com/gin-gonic/gin" cors "github.com/rs/cors/wrapper/gin" "github.com/sentrionic/valkyrie/config" "github.com/sentrionic/valkyrie/handler" "github.com/sentrionic/valkyrie/handler/middleware" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/repository" "github.com/sentrionic/valkyrie/service" "github.com/sentrionic/valkyrie/ws" "github.com/ulule/limiter/v3" mgin "github.com/ulule/limiter/v3/drivers/middleware/gin" sredis "github.com/ulule/limiter/v3/drivers/store/redis" "log" "net/http" "time" ) func inject(d *dataSources, cfg config.Config) (*gin.Engine, error) { log.Println("Injecting data sources") // Repository layer userRepository := repository.NewUserRepository(d.DB) friendRepository := repository.NewFriendRepository(d.DB) guildRepository := repository.NewGuildRepository(d.DB) channelRepository := repository.NewChannelRepository(d.DB) messageRepository := repository.NewMessageRepository(d.DB) fileRepository := repository.NewFileRepository(d.S3Session, cfg.BucketName) redisRepository := repository.NewRedisRepository(d.RedisClient) mailRepository := repository.NewMailRepository(cfg.GmailUser, cfg.GmailPassword, cfg.CorsOrigin) // Service Layer userService := service.NewUserService(&service.USConfig{ UserRepository: userRepository, FileRepository: fileRepository, RedisRepository: redisRepository, MailRepository: mailRepository, }) friendService := service.NewFriendService(&service.FSConfig{ UserRepository: userRepository, FriendRepository: friendRepository, }) guildService := service.NewGuildService(&service.GSConfig{ UserRepository: userRepository, FileRepository: fileRepository, RedisRepository: redisRepository, GuildRepository: guildRepository, ChannelRepository: channelRepository, }) channelService := service.NewChannelService(&service.CSConfig{ ChannelRepository: channelRepository, GuildRepository: guildRepository, }) messageService := service.NewMessageService(&service.MSConfig{ MessageRepository: messageRepository, FileRepository: fileRepository, }) // initialize gin.Engine router := gin.Default() // set cors settings c := cors.New(cors.Options{ AllowedOrigins: []string{cfg.CorsOrigin}, AllowCredentials: true, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, }) router.Use(c) redisURL := d.RedisClient.Options().Addr password := d.RedisClient.Options().Password // initialize session store store, err := redis.NewStore(10, "tcp", redisURL, password, []byte(cfg.SessionSecret)) if err != nil { return nil, fmt.Errorf("could not initialize redis session store: %w", err) } store.Options(sessions.Options{ Domain: cfg.Domain, MaxAge: 60 * 60 * 24 * 7, // 7 days Secure: gin.Mode() == gin.ReleaseMode, HttpOnly: true, Path: "/", SameSite: http.SameSiteLaxMode, }) router.Use(sessions.Sessions(model.CookieName, store)) // add rate limit rate := limiter.Rate{ Period: 1 * time.Hour, Limit: 1500, } limitStore, _ := sredis.NewStore(d.RedisClient) rateLimiter := mgin.NewMiddleware(limiter.New(limitStore, rate)) router.Use(rateLimiter) // Websockets Setup hub := ws.NewWebsocketHub(&ws.Config{ UserService: userService, GuildService: guildService, ChannelService: channelService, Redis: d.RedisClient, }) go hub.Run() router.GET("/ws", middleware.AuthUser(), func(c *gin.Context) { ws.ServeWs(hub, c) }) socketService := service.NewSocketService(&service.SSConfig{ Hub: *hub, GuildRepository: guildRepository, ChannelRepository: channelRepository, }) handler.NewHandler(&handler.Config{ R: router, UserService: userService, FriendService: friendService, GuildService: guildService, ChannelService: channelService, MessageService: messageService, SocketService: socketService, TimeoutDuration: time.Duration(cfg.HandlerTimeOut) * time.Second, MaxBodyBytes: cfg.MaxBodyBytes, }) return router, nil } ================================================ FILE: server/main.go ================================================ package main import ( "context" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/sentrionic/valkyrie/config" "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) // @title Valkyrie API // @version 1.0 // @description Valkyrie REST API Specs. This service uses sessions for authentication // @license.name Apache 2.0 // @host localhost: // @BasePath /api func main() { log.Println("Starting server...") // Load dev env from .env file if gin.Mode() != gin.ReleaseMode { err := godotenv.Load() if err != nil { log.Fatalln("Error loading .env file") } } ctx := context.Background() cfg, err := config.LoadConfig(ctx) if err != nil { log.Fatalf("Could not load the config: %v\n", err) } // initialize data sources ds, err := initDS(ctx, cfg) if err != nil { log.Fatalf("Unable to initialize data sources: %v\n", err) } router, err := inject(ds, cfg) if err != nil { log.Fatalf("Failure to inject data sources: %v\n", err) } srv := &http.Server{ Addr: ":" + cfg.Port, Handler: router, } // Graceful server shutdown - https://github.com/gin-gonic/examples/blob/master/graceful-shutdown/graceful-shutdown/server.go go func() { if err = srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Failed to initialize server: %v\n", err) } }() log.Printf("Listening on port %v\n", srv.Addr) // Wait for kill signal of channel quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) // This blocks until a signal is passed into the quit channel <-quit // The context is used to inform the server it has 5 seconds to finish // the request it is currently handling ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // shutdown data sources if err := ds.close(); err != nil { log.Fatalf("A problem occurred gracefully shutting down data sources: %v\n", err) } // Shutdown server log.Println("Shutting down server...") if err = srv.Shutdown(ctx); err != nil { log.Fatalf("Server forced to shutdown: %v\n", err) } } ================================================ FILE: server/mocks/ChannelRepository.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // ChannelRepository is an autogenerated mock type for the ChannelRepository type type ChannelRepository struct { mock.Mock } // AddDMChannelMembers provides a mock function with given fields: members func (_m *ChannelRepository) AddDMChannelMembers(members []model.DMMember) error { ret := _m.Called(members) var r0 error if rf, ok := ret.Get(0).(func([]model.DMMember) error); ok { r0 = rf(members) } else { r0 = ret.Error(0) } return r0 } // AddPrivateChannelMembers provides a mock function with given fields: memberIds, channelId func (_m *ChannelRepository) AddPrivateChannelMembers(memberIds []string, channelId string) error { ret := _m.Called(memberIds, channelId) var r0 error if rf, ok := ret.Get(0).(func([]string, string) error); ok { r0 = rf(memberIds, channelId) } else { r0 = ret.Error(0) } return r0 } // CleanPCMembers provides a mock function with given fields: channelId func (_m *ChannelRepository) CleanPCMembers(channelId string) error { ret := _m.Called(channelId) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(channelId) } else { r0 = ret.Error(0) } return r0 } // Create provides a mock function with given fields: channel func (_m *ChannelRepository) Create(channel *model.Channel) (*model.Channel, error) { ret := _m.Called(channel) var r0 *model.Channel if rf, ok := ret.Get(0).(func(*model.Channel) *model.Channel); ok { r0 = rf(channel) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Channel) } } var r1 error if rf, ok := ret.Get(1).(func(*model.Channel) error); ok { r1 = rf(channel) } else { r1 = ret.Error(1) } return r0, r1 } // DeleteChannel provides a mock function with given fields: channel func (_m *ChannelRepository) DeleteChannel(channel *model.Channel) error { ret := _m.Called(channel) var r0 error if rf, ok := ret.Get(0).(func(*model.Channel) error); ok { r0 = rf(channel) } else { r0 = ret.Error(0) } return r0 } // FindDMByUserAndChannelId provides a mock function with given fields: channelId, userId func (_m *ChannelRepository) FindDMByUserAndChannelId(channelId string, userId string) (string, error) { ret := _m.Called(channelId, userId) var r0 string if rf, ok := ret.Get(0).(func(string, string) string); ok { r0 = rf(channelId, userId) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(channelId, userId) } else { r1 = ret.Error(1) } return r0, r1 } // Get provides a mock function with given fields: userId, guildId func (_m *ChannelRepository) Get(userId string, guildId string) (*[]model.ChannelResponse, error) { ret := _m.Called(userId, guildId) var r0 *[]model.ChannelResponse if rf, ok := ret.Get(0).(func(string, string) *[]model.ChannelResponse); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.ChannelResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetById provides a mock function with given fields: channelId func (_m *ChannelRepository) GetById(channelId string) (*model.Channel, error) { ret := _m.Called(channelId) var r0 *model.Channel if rf, ok := ret.Get(0).(func(string) *model.Channel); ok { r0 = rf(channelId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Channel) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(channelId) } else { r1 = ret.Error(1) } return r0, r1 } // GetDMMemberIds provides a mock function with given fields: channelId func (_m *ChannelRepository) GetDMMemberIds(channelId string) (*[]string, error) { ret := _m.Called(channelId) var r0 *[]string if rf, ok := ret.Get(0).(func(string) *[]string); ok { r0 = rf(channelId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]string) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(channelId) } else { r1 = ret.Error(1) } return r0, r1 } // GetDirectMessageChannel provides a mock function with given fields: userId, memberId func (_m *ChannelRepository) GetDirectMessageChannel(userId string, memberId string) (*string, error) { ret := _m.Called(userId, memberId) var r0 *string if rf, ok := ret.Get(0).(func(string, string) *string); ok { r0 = rf(userId, memberId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*string) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, memberId) } else { r1 = ret.Error(1) } return r0, r1 } // GetDirectMessages provides a mock function with given fields: userId func (_m *ChannelRepository) GetDirectMessages(userId string) (*[]model.DirectMessage, error) { ret := _m.Called(userId) var r0 *[]model.DirectMessage if rf, ok := ret.Get(0).(func(string) *[]model.DirectMessage); ok { r0 = rf(userId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.DirectMessage) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(userId) } else { r1 = ret.Error(1) } return r0, r1 } // GetGuildDefault provides a mock function with given fields: guildId func (_m *ChannelRepository) GetGuildDefault(guildId string) (*model.Channel, error) { ret := _m.Called(guildId) var r0 *model.Channel if rf, ok := ret.Get(0).(func(string) *model.Channel); ok { r0 = rf(guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Channel) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetPrivateChannelMembers provides a mock function with given fields: channelId func (_m *ChannelRepository) GetPrivateChannelMembers(channelId string) (*[]string, error) { ret := _m.Called(channelId) var r0 *[]string if rf, ok := ret.Get(0).(func(string) *[]string); ok { r0 = rf(channelId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]string) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(channelId) } else { r1 = ret.Error(1) } return r0, r1 } // OpenDMForAll provides a mock function with given fields: dmId func (_m *ChannelRepository) OpenDMForAll(dmId string) error { ret := _m.Called(dmId) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(dmId) } else { r0 = ret.Error(0) } return r0 } // RemovePrivateChannelMembers provides a mock function with given fields: memberIds, channelId func (_m *ChannelRepository) RemovePrivateChannelMembers(memberIds []string, channelId string) error { ret := _m.Called(memberIds, channelId) var r0 error if rf, ok := ret.Get(0).(func([]string, string) error); ok { r0 = rf(memberIds, channelId) } else { r0 = ret.Error(0) } return r0 } // SetDirectMessageStatus provides a mock function with given fields: dmId, userId, isOpen func (_m *ChannelRepository) SetDirectMessageStatus(dmId string, userId string, isOpen bool) error { ret := _m.Called(dmId, userId, isOpen) var r0 error if rf, ok := ret.Get(0).(func(string, string, bool) error); ok { r0 = rf(dmId, userId, isOpen) } else { r0 = ret.Error(0) } return r0 } // UpdateChannel provides a mock function with given fields: channel func (_m *ChannelRepository) UpdateChannel(channel *model.Channel) error { ret := _m.Called(channel) var r0 error if rf, ok := ret.Get(0).(func(*model.Channel) error); ok { r0 = rf(channel) } else { r0 = ret.Error(0) } return r0 } // NewChannelRepository creates a new instance of ChannelRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewChannelRepository(t testing.TB) *ChannelRepository { mock := &ChannelRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/ChannelService.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // ChannelService is an autogenerated mock type for the ChannelService type type ChannelService struct { mock.Mock } // AddDMChannelMembers provides a mock function with given fields: memberIds, channelId, userId func (_m *ChannelService) AddDMChannelMembers(memberIds []string, channelId string, userId string) error { ret := _m.Called(memberIds, channelId, userId) var r0 error if rf, ok := ret.Get(0).(func([]string, string, string) error); ok { r0 = rf(memberIds, channelId, userId) } else { r0 = ret.Error(0) } return r0 } // AddPrivateChannelMembers provides a mock function with given fields: memberIds, channelId func (_m *ChannelService) AddPrivateChannelMembers(memberIds []string, channelId string) error { ret := _m.Called(memberIds, channelId) var r0 error if rf, ok := ret.Get(0).(func([]string, string) error); ok { r0 = rf(memberIds, channelId) } else { r0 = ret.Error(0) } return r0 } // CleanPCMembers provides a mock function with given fields: channelId func (_m *ChannelService) CleanPCMembers(channelId string) error { ret := _m.Called(channelId) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(channelId) } else { r0 = ret.Error(0) } return r0 } // CreateChannel provides a mock function with given fields: channel func (_m *ChannelService) CreateChannel(channel *model.Channel) (*model.Channel, error) { ret := _m.Called(channel) var r0 *model.Channel if rf, ok := ret.Get(0).(func(*model.Channel) *model.Channel); ok { r0 = rf(channel) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Channel) } } var r1 error if rf, ok := ret.Get(1).(func(*model.Channel) error); ok { r1 = rf(channel) } else { r1 = ret.Error(1) } return r0, r1 } // DeleteChannel provides a mock function with given fields: channel func (_m *ChannelService) DeleteChannel(channel *model.Channel) error { ret := _m.Called(channel) var r0 error if rf, ok := ret.Get(0).(func(*model.Channel) error); ok { r0 = rf(channel) } else { r0 = ret.Error(0) } return r0 } // Get provides a mock function with given fields: channelId func (_m *ChannelService) Get(channelId string) (*model.Channel, error) { ret := _m.Called(channelId) var r0 *model.Channel if rf, ok := ret.Get(0).(func(string) *model.Channel); ok { r0 = rf(channelId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Channel) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(channelId) } else { r1 = ret.Error(1) } return r0, r1 } // GetChannels provides a mock function with given fields: userId, guildId func (_m *ChannelService) GetChannels(userId string, guildId string) (*[]model.ChannelResponse, error) { ret := _m.Called(userId, guildId) var r0 *[]model.ChannelResponse if rf, ok := ret.Get(0).(func(string, string) *[]model.ChannelResponse); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.ChannelResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetDMByUserAndChannel provides a mock function with given fields: userId, channelId func (_m *ChannelService) GetDMByUserAndChannel(userId string, channelId string) (string, error) { ret := _m.Called(userId, channelId) var r0 string if rf, ok := ret.Get(0).(func(string, string) string); ok { r0 = rf(userId, channelId) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, channelId) } else { r1 = ret.Error(1) } return r0, r1 } // GetDirectMessageChannel provides a mock function with given fields: userId, memberId func (_m *ChannelService) GetDirectMessageChannel(userId string, memberId string) (*string, error) { ret := _m.Called(userId, memberId) var r0 *string if rf, ok := ret.Get(0).(func(string, string) *string); ok { r0 = rf(userId, memberId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*string) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, memberId) } else { r1 = ret.Error(1) } return r0, r1 } // GetDirectMessages provides a mock function with given fields: userId func (_m *ChannelService) GetDirectMessages(userId string) (*[]model.DirectMessage, error) { ret := _m.Called(userId) var r0 *[]model.DirectMessage if rf, ok := ret.Get(0).(func(string) *[]model.DirectMessage); ok { r0 = rf(userId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.DirectMessage) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(userId) } else { r1 = ret.Error(1) } return r0, r1 } // GetPrivateChannelMembers provides a mock function with given fields: channelId func (_m *ChannelService) GetPrivateChannelMembers(channelId string) (*[]string, error) { ret := _m.Called(channelId) var r0 *[]string if rf, ok := ret.Get(0).(func(string) *[]string); ok { r0 = rf(channelId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]string) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(channelId) } else { r1 = ret.Error(1) } return r0, r1 } // IsChannelMember provides a mock function with given fields: channel, userId func (_m *ChannelService) IsChannelMember(channel *model.Channel, userId string) error { ret := _m.Called(channel, userId) var r0 error if rf, ok := ret.Get(0).(func(*model.Channel, string) error); ok { r0 = rf(channel, userId) } else { r0 = ret.Error(0) } return r0 } // OpenDMForAll provides a mock function with given fields: dmId func (_m *ChannelService) OpenDMForAll(dmId string) error { ret := _m.Called(dmId) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(dmId) } else { r0 = ret.Error(0) } return r0 } // RemovePrivateChannelMembers provides a mock function with given fields: memberIds, channelId func (_m *ChannelService) RemovePrivateChannelMembers(memberIds []string, channelId string) error { ret := _m.Called(memberIds, channelId) var r0 error if rf, ok := ret.Get(0).(func([]string, string) error); ok { r0 = rf(memberIds, channelId) } else { r0 = ret.Error(0) } return r0 } // SetDirectMessageStatus provides a mock function with given fields: dmId, userId, isOpen func (_m *ChannelService) SetDirectMessageStatus(dmId string, userId string, isOpen bool) error { ret := _m.Called(dmId, userId, isOpen) var r0 error if rf, ok := ret.Get(0).(func(string, string, bool) error); ok { r0 = rf(dmId, userId, isOpen) } else { r0 = ret.Error(0) } return r0 } // UpdateChannel provides a mock function with given fields: channel func (_m *ChannelService) UpdateChannel(channel *model.Channel) error { ret := _m.Called(channel) var r0 error if rf, ok := ret.Get(0).(func(*model.Channel) error); ok { r0 = rf(channel) } else { r0 = ret.Error(0) } return r0 } // NewChannelService creates a new instance of ChannelService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewChannelService(t testing.TB) *ChannelService { mock := &ChannelService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/FileRepository.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( mock "github.com/stretchr/testify/mock" multipart "mime/multipart" testing "testing" ) // FileRepository is an autogenerated mock type for the FileRepository type type FileRepository struct { mock.Mock } // DeleteImage provides a mock function with given fields: key func (_m *FileRepository) DeleteImage(key string) error { ret := _m.Called(key) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(key) } else { r0 = ret.Error(0) } return r0 } // UploadAvatar provides a mock function with given fields: header, directory func (_m *FileRepository) UploadAvatar(header *multipart.FileHeader, directory string) (string, error) { ret := _m.Called(header, directory) var r0 string if rf, ok := ret.Get(0).(func(*multipart.FileHeader, string) string); ok { r0 = rf(header, directory) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(*multipart.FileHeader, string) error); ok { r1 = rf(header, directory) } else { r1 = ret.Error(1) } return r0, r1 } // UploadFile provides a mock function with given fields: header, directory, filename, mimetype func (_m *FileRepository) UploadFile(header *multipart.FileHeader, directory string, filename string, mimetype string) (string, error) { ret := _m.Called(header, directory, filename, mimetype) var r0 string if rf, ok := ret.Get(0).(func(*multipart.FileHeader, string, string, string) string); ok { r0 = rf(header, directory, filename, mimetype) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(*multipart.FileHeader, string, string, string) error); ok { r1 = rf(header, directory, filename, mimetype) } else { r1 = ret.Error(1) } return r0, r1 } // NewFileRepository creates a new instance of FileRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewFileRepository(t testing.TB) *FileRepository { mock := &FileRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/FriendRepository.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // FriendRepository is an autogenerated mock type for the FriendRepository type type FriendRepository struct { mock.Mock } // DeleteRequest provides a mock function with given fields: memberId, userId func (_m *FriendRepository) DeleteRequest(memberId string, userId string) error { ret := _m.Called(memberId, userId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(memberId, userId) } else { r0 = ret.Error(0) } return r0 } // FindByID provides a mock function with given fields: id func (_m *FriendRepository) FindByID(id string) (*model.User, error) { ret := _m.Called(id) var r0 *model.User if rf, ok := ret.Get(0).(func(string) *model.User); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // FriendsList provides a mock function with given fields: id func (_m *FriendRepository) FriendsList(id string) (*[]model.Friend, error) { ret := _m.Called(id) var r0 *[]model.Friend if rf, ok := ret.Get(0).(func(string) *[]model.Friend); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.Friend) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // RemoveFriend provides a mock function with given fields: memberId, userId func (_m *FriendRepository) RemoveFriend(memberId string, userId string) error { ret := _m.Called(memberId, userId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(memberId, userId) } else { r0 = ret.Error(0) } return r0 } // RequestList provides a mock function with given fields: id func (_m *FriendRepository) RequestList(id string) (*[]model.FriendRequest, error) { ret := _m.Called(id) var r0 *[]model.FriendRequest if rf, ok := ret.Get(0).(func(string) *[]model.FriendRequest); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.FriendRequest) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // Save provides a mock function with given fields: user func (_m *FriendRepository) Save(user *model.User) error { ret := _m.Called(user) var r0 error if rf, ok := ret.Get(0).(func(*model.User) error); ok { r0 = rf(user) } else { r0 = ret.Error(0) } return r0 } // NewFriendRepository creates a new instance of FriendRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewFriendRepository(t testing.TB) *FriendRepository { mock := &FriendRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/FriendService.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // FriendService is an autogenerated mock type for the FriendService type type FriendService struct { mock.Mock } // DeleteRequest provides a mock function with given fields: memberId, userId func (_m *FriendService) DeleteRequest(memberId string, userId string) error { ret := _m.Called(memberId, userId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(memberId, userId) } else { r0 = ret.Error(0) } return r0 } // GetFriends provides a mock function with given fields: id func (_m *FriendService) GetFriends(id string) (*[]model.Friend, error) { ret := _m.Called(id) var r0 *[]model.Friend if rf, ok := ret.Get(0).(func(string) *[]model.Friend); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.Friend) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // GetMemberById provides a mock function with given fields: id func (_m *FriendService) GetMemberById(id string) (*model.User, error) { ret := _m.Called(id) var r0 *model.User if rf, ok := ret.Get(0).(func(string) *model.User); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // GetRequests provides a mock function with given fields: id func (_m *FriendService) GetRequests(id string) (*[]model.FriendRequest, error) { ret := _m.Called(id) var r0 *[]model.FriendRequest if rf, ok := ret.Get(0).(func(string) *[]model.FriendRequest); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.FriendRequest) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // RemoveFriend provides a mock function with given fields: memberId, userId func (_m *FriendService) RemoveFriend(memberId string, userId string) error { ret := _m.Called(memberId, userId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(memberId, userId) } else { r0 = ret.Error(0) } return r0 } // SaveRequests provides a mock function with given fields: user func (_m *FriendService) SaveRequests(user *model.User) error { ret := _m.Called(user) var r0 error if rf, ok := ret.Get(0).(func(*model.User) error); ok { r0 = rf(user) } else { r0 = ret.Error(0) } return r0 } // NewFriendService creates a new instance of FriendService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewFriendService(t testing.TB) *FriendService { mock := &FriendService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/GuildRepository.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // GuildRepository is an autogenerated mock type for the GuildRepository type type GuildRepository struct { mock.Mock } // Create provides a mock function with given fields: guild func (_m *GuildRepository) Create(guild *model.Guild) (*model.Guild, error) { ret := _m.Called(guild) var r0 *model.Guild if rf, ok := ret.Get(0).(func(*model.Guild) *model.Guild); ok { r0 = rf(guild) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Guild) } } var r1 error if rf, ok := ret.Get(1).(func(*model.Guild) error); ok { r1 = rf(guild) } else { r1 = ret.Error(1) } return r0, r1 } // Delete provides a mock function with given fields: guildId func (_m *GuildRepository) Delete(guildId string) error { ret := _m.Called(guildId) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(guildId) } else { r0 = ret.Error(0) } return r0 } // FindByID provides a mock function with given fields: id func (_m *GuildRepository) FindByID(id string) (*model.Guild, error) { ret := _m.Called(id) var r0 *model.Guild if rf, ok := ret.Get(0).(func(string) *model.Guild); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Guild) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // FindUserByID provides a mock function with given fields: uid func (_m *GuildRepository) FindUserByID(uid string) (*model.User, error) { ret := _m.Called(uid) var r0 *model.User if rf, ok := ret.Get(0).(func(string) *model.User); ok { r0 = rf(uid) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(uid) } else { r1 = ret.Error(1) } return r0, r1 } // FindUsersByIds provides a mock function with given fields: ids, guildId func (_m *GuildRepository) FindUsersByIds(ids []string, guildId string) (*[]model.User, error) { ret := _m.Called(ids, guildId) var r0 *[]model.User if rf, ok := ret.Get(0).(func([]string, string) *[]model.User); ok { r0 = rf(ids, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.User) } } var r1 error if rf, ok := ret.Get(1).(func([]string, string) error); ok { r1 = rf(ids, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetBanList provides a mock function with given fields: guildId func (_m *GuildRepository) GetBanList(guildId string) (*[]model.BanResponse, error) { ret := _m.Called(guildId) var r0 *[]model.BanResponse if rf, ok := ret.Get(0).(func(string) *[]model.BanResponse); ok { r0 = rf(guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.BanResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetMember provides a mock function with given fields: userId, guildId func (_m *GuildRepository) GetMember(userId string, guildId string) (*model.User, error) { ret := _m.Called(userId, guildId) var r0 *model.User if rf, ok := ret.Get(0).(func(string, string) *model.User); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetMemberIds provides a mock function with given fields: guildId func (_m *GuildRepository) GetMemberIds(guildId string) (*[]string, error) { ret := _m.Called(guildId) var r0 *[]string if rf, ok := ret.Get(0).(func(string) *[]string); ok { r0 = rf(guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]string) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetMemberSettings provides a mock function with given fields: userId, guildId func (_m *GuildRepository) GetMemberSettings(userId string, guildId string) (*model.MemberSettings, error) { ret := _m.Called(userId, guildId) var r0 *model.MemberSettings if rf, ok := ret.Get(0).(func(string, string) *model.MemberSettings); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.MemberSettings) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetVCMember provides a mock function with given fields: userId, guildId func (_m *GuildRepository) GetVCMember(userId string, guildId string) (*model.VCMember, error) { ret := _m.Called(userId, guildId) var r0 *model.VCMember if rf, ok := ret.Get(0).(func(string, string) *model.VCMember); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.VCMember) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GuildMembers provides a mock function with given fields: userId, guildId func (_m *GuildRepository) GuildMembers(userId string, guildId string) (*[]model.MemberResponse, error) { ret := _m.Called(userId, guildId) var r0 *[]model.MemberResponse if rf, ok := ret.Get(0).(func(string, string) *[]model.MemberResponse); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.MemberResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // List provides a mock function with given fields: uid func (_m *GuildRepository) List(uid string) (*[]model.GuildResponse, error) { ret := _m.Called(uid) var r0 *[]model.GuildResponse if rf, ok := ret.Get(0).(func(string) *[]model.GuildResponse); ok { r0 = rf(uid) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.GuildResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(uid) } else { r1 = ret.Error(1) } return r0, r1 } // RemoveMember provides a mock function with given fields: userId, guildId func (_m *GuildRepository) RemoveMember(userId string, guildId string) error { ret := _m.Called(userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(userId, guildId) } else { r0 = ret.Error(0) } return r0 } // RemoveVCMember provides a mock function with given fields: userId, guildId func (_m *GuildRepository) RemoveVCMember(userId string, guildId string) error { ret := _m.Called(userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(userId, guildId) } else { r0 = ret.Error(0) } return r0 } // Save provides a mock function with given fields: guild func (_m *GuildRepository) Save(guild *model.Guild) error { ret := _m.Called(guild) var r0 error if rf, ok := ret.Get(0).(func(*model.Guild) error); ok { r0 = rf(guild) } else { r0 = ret.Error(0) } return r0 } // UnbanMember provides a mock function with given fields: userId, guildId func (_m *GuildRepository) UnbanMember(userId string, guildId string) error { ret := _m.Called(userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(userId, guildId) } else { r0 = ret.Error(0) } return r0 } // UpdateMemberLastSeen provides a mock function with given fields: userId, guildId func (_m *GuildRepository) UpdateMemberLastSeen(userId string, guildId string) error { ret := _m.Called(userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(userId, guildId) } else { r0 = ret.Error(0) } return r0 } // UpdateMemberSettings provides a mock function with given fields: settings, userId, guildId func (_m *GuildRepository) UpdateMemberSettings(settings *model.MemberSettings, userId string, guildId string) error { ret := _m.Called(settings, userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(*model.MemberSettings, string, string) error); ok { r0 = rf(settings, userId, guildId) } else { r0 = ret.Error(0) } return r0 } // UpdateVCMember provides a mock function with given fields: isMuted, isDeafened, userId, guildId func (_m *GuildRepository) UpdateVCMember(isMuted bool, isDeafened bool, userId string, guildId string) error { ret := _m.Called(isMuted, isDeafened, userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(bool, bool, string, string) error); ok { r0 = rf(isMuted, isDeafened, userId, guildId) } else { r0 = ret.Error(0) } return r0 } // VCMembers provides a mock function with given fields: guildId func (_m *GuildRepository) VCMembers(guildId string) (*[]model.VCMemberResponse, error) { ret := _m.Called(guildId) var r0 *[]model.VCMemberResponse if rf, ok := ret.Get(0).(func(string) *[]model.VCMemberResponse); ok { r0 = rf(guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.VCMemberResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(guildId) } else { r1 = ret.Error(1) } return r0, r1 } // NewGuildRepository creates a new instance of GuildRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewGuildRepository(t testing.TB) *GuildRepository { mock := &GuildRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/GuildService.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( context "context" model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // GuildService is an autogenerated mock type for the GuildService type type GuildService struct { mock.Mock } // CreateGuild provides a mock function with given fields: guild func (_m *GuildService) CreateGuild(guild *model.Guild) (*model.Guild, error) { ret := _m.Called(guild) var r0 *model.Guild if rf, ok := ret.Get(0).(func(*model.Guild) *model.Guild); ok { r0 = rf(guild) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Guild) } } var r1 error if rf, ok := ret.Get(1).(func(*model.Guild) error); ok { r1 = rf(guild) } else { r1 = ret.Error(1) } return r0, r1 } // DeleteGuild provides a mock function with given fields: guildId func (_m *GuildService) DeleteGuild(guildId string) error { ret := _m.Called(guildId) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(guildId) } else { r0 = ret.Error(0) } return r0 } // FindUsersByIds provides a mock function with given fields: ids, guildId func (_m *GuildService) FindUsersByIds(ids []string, guildId string) (*[]model.User, error) { ret := _m.Called(ids, guildId) var r0 *[]model.User if rf, ok := ret.Get(0).(func([]string, string) *[]model.User); ok { r0 = rf(ids, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.User) } } var r1 error if rf, ok := ret.Get(1).(func([]string, string) error); ok { r1 = rf(ids, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GenerateInviteLink provides a mock function with given fields: ctx, guildId, isPermanent func (_m *GuildService) GenerateInviteLink(ctx context.Context, guildId string, isPermanent bool) (string, error) { ret := _m.Called(ctx, guildId, isPermanent) var r0 string if rf, ok := ret.Get(0).(func(context.Context, string, bool) string); ok { r0 = rf(ctx, guildId, isPermanent) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok { r1 = rf(ctx, guildId, isPermanent) } else { r1 = ret.Error(1) } return r0, r1 } // GetBanList provides a mock function with given fields: guildId func (_m *GuildService) GetBanList(guildId string) (*[]model.BanResponse, error) { ret := _m.Called(guildId) var r0 *[]model.BanResponse if rf, ok := ret.Get(0).(func(string) *[]model.BanResponse); ok { r0 = rf(guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.BanResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetDefaultChannel provides a mock function with given fields: guildId func (_m *GuildService) GetDefaultChannel(guildId string) (*model.Channel, error) { ret := _m.Called(guildId) var r0 *model.Channel if rf, ok := ret.Get(0).(func(string) *model.Channel); ok { r0 = rf(guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Channel) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetGuild provides a mock function with given fields: id func (_m *GuildService) GetGuild(id string) (*model.Guild, error) { ret := _m.Called(id) var r0 *model.Guild if rf, ok := ret.Get(0).(func(string) *model.Guild); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Guild) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // GetGuildIdFromInvite provides a mock function with given fields: ctx, token func (_m *GuildService) GetGuildIdFromInvite(ctx context.Context, token string) (string, error) { ret := _m.Called(ctx, token) var r0 string if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { r0 = rf(ctx, token) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = rf(ctx, token) } else { r1 = ret.Error(1) } return r0, r1 } // GetGuildMembers provides a mock function with given fields: userId, guildId func (_m *GuildService) GetGuildMembers(userId string, guildId string) (*[]model.MemberResponse, error) { ret := _m.Called(userId, guildId) var r0 *[]model.MemberResponse if rf, ok := ret.Get(0).(func(string, string) *[]model.MemberResponse); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.MemberResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetMemberSettings provides a mock function with given fields: userId, guildId func (_m *GuildService) GetMemberSettings(userId string, guildId string) (*model.MemberSettings, error) { ret := _m.Called(userId, guildId) var r0 *model.MemberSettings if rf, ok := ret.Get(0).(func(string, string) *model.MemberSettings); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.MemberSettings) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetUser provides a mock function with given fields: uid func (_m *GuildService) GetUser(uid string) (*model.User, error) { ret := _m.Called(uid) var r0 *model.User if rf, ok := ret.Get(0).(func(string) *model.User); ok { r0 = rf(uid) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(uid) } else { r1 = ret.Error(1) } return r0, r1 } // GetUserGuilds provides a mock function with given fields: uid func (_m *GuildService) GetUserGuilds(uid string) (*[]model.GuildResponse, error) { ret := _m.Called(uid) var r0 *[]model.GuildResponse if rf, ok := ret.Get(0).(func(string) *[]model.GuildResponse); ok { r0 = rf(uid) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.GuildResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(uid) } else { r1 = ret.Error(1) } return r0, r1 } // GetVCMember provides a mock function with given fields: userId, guildId func (_m *GuildService) GetVCMember(userId string, guildId string) (*model.VCMember, error) { ret := _m.Called(userId, guildId) var r0 *model.VCMember if rf, ok := ret.Get(0).(func(string, string) *model.VCMember); ok { r0 = rf(userId, guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.VCMember) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(userId, guildId) } else { r1 = ret.Error(1) } return r0, r1 } // GetVCMembers provides a mock function with given fields: guildId func (_m *GuildService) GetVCMembers(guildId string) (*[]model.VCMemberResponse, error) { ret := _m.Called(guildId) var r0 *[]model.VCMemberResponse if rf, ok := ret.Get(0).(func(string) *[]model.VCMemberResponse); ok { r0 = rf(guildId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.VCMemberResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(guildId) } else { r1 = ret.Error(1) } return r0, r1 } // InvalidateInvites provides a mock function with given fields: ctx, guild func (_m *GuildService) InvalidateInvites(ctx context.Context, guild *model.Guild) { _m.Called(ctx, guild) } // RemoveMember provides a mock function with given fields: userId, guildId func (_m *GuildService) RemoveMember(userId string, guildId string) error { ret := _m.Called(userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(userId, guildId) } else { r0 = ret.Error(0) } return r0 } // RemoveVCMember provides a mock function with given fields: userId, guildId func (_m *GuildService) RemoveVCMember(userId string, guildId string) error { ret := _m.Called(userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(userId, guildId) } else { r0 = ret.Error(0) } return r0 } // UnbanMember provides a mock function with given fields: userId, guildId func (_m *GuildService) UnbanMember(userId string, guildId string) error { ret := _m.Called(userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(userId, guildId) } else { r0 = ret.Error(0) } return r0 } // UpdateGuild provides a mock function with given fields: guild func (_m *GuildService) UpdateGuild(guild *model.Guild) error { ret := _m.Called(guild) var r0 error if rf, ok := ret.Get(0).(func(*model.Guild) error); ok { r0 = rf(guild) } else { r0 = ret.Error(0) } return r0 } // UpdateMemberLastSeen provides a mock function with given fields: userId, guildId func (_m *GuildService) UpdateMemberLastSeen(userId string, guildId string) error { ret := _m.Called(userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(userId, guildId) } else { r0 = ret.Error(0) } return r0 } // UpdateMemberSettings provides a mock function with given fields: settings, userId, guildId func (_m *GuildService) UpdateMemberSettings(settings *model.MemberSettings, userId string, guildId string) error { ret := _m.Called(settings, userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(*model.MemberSettings, string, string) error); ok { r0 = rf(settings, userId, guildId) } else { r0 = ret.Error(0) } return r0 } // UpdateVCMember provides a mock function with given fields: isMuted, isDeafened, userId, guildId func (_m *GuildService) UpdateVCMember(isMuted bool, isDeafened bool, userId string, guildId string) error { ret := _m.Called(isMuted, isDeafened, userId, guildId) var r0 error if rf, ok := ret.Get(0).(func(bool, bool, string, string) error); ok { r0 = rf(isMuted, isDeafened, userId, guildId) } else { r0 = ret.Error(0) } return r0 } // NewGuildService creates a new instance of GuildService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewGuildService(t testing.TB) *GuildService { mock := &GuildService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/MailRepository.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( mock "github.com/stretchr/testify/mock" testing "testing" ) // MailRepository is an autogenerated mock type for the MailRepository type type MailRepository struct { mock.Mock } // SendResetMail provides a mock function with given fields: email, html func (_m *MailRepository) SendResetMail(email string, html string) error { ret := _m.Called(email, html) var r0 error if rf, ok := ret.Get(0).(func(string, string) error); ok { r0 = rf(email, html) } else { r0 = ret.Error(0) } return r0 } // NewMailRepository creates a new instance of MailRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewMailRepository(t testing.TB) *MailRepository { mock := &MailRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/MessageRepository.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // MessageRepository is an autogenerated mock type for the MessageRepository type type MessageRepository struct { mock.Mock } // CreateMessage provides a mock function with given fields: params func (_m *MessageRepository) CreateMessage(params *model.Message) (*model.Message, error) { ret := _m.Called(params) var r0 *model.Message if rf, ok := ret.Get(0).(func(*model.Message) *model.Message); ok { r0 = rf(params) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Message) } } var r1 error if rf, ok := ret.Get(1).(func(*model.Message) error); ok { r1 = rf(params) } else { r1 = ret.Error(1) } return r0, r1 } // DeleteMessage provides a mock function with given fields: message func (_m *MessageRepository) DeleteMessage(message *model.Message) error { ret := _m.Called(message) var r0 error if rf, ok := ret.Get(0).(func(*model.Message) error); ok { r0 = rf(message) } else { r0 = ret.Error(0) } return r0 } // GetById provides a mock function with given fields: messageId func (_m *MessageRepository) GetById(messageId string) (*model.Message, error) { ret := _m.Called(messageId) var r0 *model.Message if rf, ok := ret.Get(0).(func(string) *model.Message); ok { r0 = rf(messageId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Message) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(messageId) } else { r1 = ret.Error(1) } return r0, r1 } // GetMessages provides a mock function with given fields: userId, channel, cursor func (_m *MessageRepository) GetMessages(userId string, channel *model.Channel, cursor string) (*[]model.MessageResponse, error) { ret := _m.Called(userId, channel, cursor) var r0 *[]model.MessageResponse if rf, ok := ret.Get(0).(func(string, *model.Channel, string) *[]model.MessageResponse); ok { r0 = rf(userId, channel, cursor) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.MessageResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string, *model.Channel, string) error); ok { r1 = rf(userId, channel, cursor) } else { r1 = ret.Error(1) } return r0, r1 } // UpdateMessage provides a mock function with given fields: message func (_m *MessageRepository) UpdateMessage(message *model.Message) error { ret := _m.Called(message) var r0 error if rf, ok := ret.Get(0).(func(*model.Message) error); ok { r0 = rf(message) } else { r0 = ret.Error(0) } return r0 } // NewMessageRepository creates a new instance of MessageRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewMessageRepository(t testing.TB) *MessageRepository { mock := &MessageRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/MessageService.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" multipart "mime/multipart" testing "testing" ) // MessageService is an autogenerated mock type for the MessageService type type MessageService struct { mock.Mock } // CreateMessage provides a mock function with given fields: params func (_m *MessageService) CreateMessage(params *model.Message) (*model.Message, error) { ret := _m.Called(params) var r0 *model.Message if rf, ok := ret.Get(0).(func(*model.Message) *model.Message); ok { r0 = rf(params) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Message) } } var r1 error if rf, ok := ret.Get(1).(func(*model.Message) error); ok { r1 = rf(params) } else { r1 = ret.Error(1) } return r0, r1 } // DeleteMessage provides a mock function with given fields: message func (_m *MessageService) DeleteMessage(message *model.Message) error { ret := _m.Called(message) var r0 error if rf, ok := ret.Get(0).(func(*model.Message) error); ok { r0 = rf(message) } else { r0 = ret.Error(0) } return r0 } // Get provides a mock function with given fields: messageId func (_m *MessageService) Get(messageId string) (*model.Message, error) { ret := _m.Called(messageId) var r0 *model.Message if rf, ok := ret.Get(0).(func(string) *model.Message); ok { r0 = rf(messageId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Message) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(messageId) } else { r1 = ret.Error(1) } return r0, r1 } // GetMessages provides a mock function with given fields: userId, channel, cursor func (_m *MessageService) GetMessages(userId string, channel *model.Channel, cursor string) (*[]model.MessageResponse, error) { ret := _m.Called(userId, channel, cursor) var r0 *[]model.MessageResponse if rf, ok := ret.Get(0).(func(string, *model.Channel, string) *[]model.MessageResponse); ok { r0 = rf(userId, channel, cursor) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]model.MessageResponse) } } var r1 error if rf, ok := ret.Get(1).(func(string, *model.Channel, string) error); ok { r1 = rf(userId, channel, cursor) } else { r1 = ret.Error(1) } return r0, r1 } // UpdateMessage provides a mock function with given fields: message func (_m *MessageService) UpdateMessage(message *model.Message) error { ret := _m.Called(message) var r0 error if rf, ok := ret.Get(0).(func(*model.Message) error); ok { r0 = rf(message) } else { r0 = ret.Error(0) } return r0 } // UploadFile provides a mock function with given fields: header, channelId func (_m *MessageService) UploadFile(header *multipart.FileHeader, channelId string) (*model.Attachment, error) { ret := _m.Called(header, channelId) var r0 *model.Attachment if rf, ok := ret.Get(0).(func(*multipart.FileHeader, string) *model.Attachment); ok { r0 = rf(header, channelId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.Attachment) } } var r1 error if rf, ok := ret.Get(1).(func(*multipart.FileHeader, string) error); ok { r1 = rf(header, channelId) } else { r1 = ret.Error(1) } return r0, r1 } // NewMessageService creates a new instance of MessageService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewMessageService(t testing.TB) *MessageService { mock := &MessageService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/RedisRepository.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( context "context" model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // RedisRepository is an autogenerated mock type for the RedisRepository type type RedisRepository struct { mock.Mock } // GetIdFromToken provides a mock function with given fields: ctx, token func (_m *RedisRepository) GetIdFromToken(ctx context.Context, token string) (string, error) { ret := _m.Called(ctx, token) var r0 string if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { r0 = rf(ctx, token) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = rf(ctx, token) } else { r1 = ret.Error(1) } return r0, r1 } // GetInvite provides a mock function with given fields: ctx, token func (_m *RedisRepository) GetInvite(ctx context.Context, token string) (string, error) { ret := _m.Called(ctx, token) var r0 string if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { r0 = rf(ctx, token) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = rf(ctx, token) } else { r1 = ret.Error(1) } return r0, r1 } // InvalidateInvites provides a mock function with given fields: ctx, guild func (_m *RedisRepository) InvalidateInvites(ctx context.Context, guild *model.Guild) { _m.Called(ctx, guild) } // SaveInvite provides a mock function with given fields: ctx, guildId, id, isPermanent func (_m *RedisRepository) SaveInvite(ctx context.Context, guildId string, id string, isPermanent bool) error { ret := _m.Called(ctx, guildId, id, isPermanent) var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) error); ok { r0 = rf(ctx, guildId, id, isPermanent) } else { r0 = ret.Error(0) } return r0 } // SetResetToken provides a mock function with given fields: ctx, id func (_m *RedisRepository) SetResetToken(ctx context.Context, id string) (string, error) { ret := _m.Called(ctx, id) var r0 string if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { r0 = rf(ctx, id) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { r1 = rf(ctx, id) } else { r1 = ret.Error(1) } return r0, r1 } // NewRedisRepository creates a new instance of RedisRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewRedisRepository(t testing.TB) *RedisRepository { mock := &RedisRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/Request.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( testing "testing" mock "github.com/stretchr/testify/mock" ) // Request is an autogenerated mock type for the Request type type Request struct { mock.Mock } // validate provides a mock function with given fields: func (_m *Request) validate() error { ret := _m.Called() var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() } else { r0 = ret.Error(0) } return r0 } // NewRequest creates a new instance of Request. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewRequest(t testing.TB) *Request { mock := &Request{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/SocketService.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // SocketService is an autogenerated mock type for the SocketService type type SocketService struct { mock.Mock } // EmitAddFriend provides a mock function with given fields: user, member func (_m *SocketService) EmitAddFriend(user *model.User, member *model.User) { _m.Called(user, member) } // EmitAddFriendRequest provides a mock function with given fields: room, request func (_m *SocketService) EmitAddFriendRequest(room string, request *model.FriendRequest) { _m.Called(room, request) } // EmitAddMember provides a mock function with given fields: room, member func (_m *SocketService) EmitAddMember(room string, member *model.User) { _m.Called(room, member) } // EmitDeleteChannel provides a mock function with given fields: channel func (_m *SocketService) EmitDeleteChannel(channel *model.Channel) { _m.Called(channel) } // EmitDeleteGuild provides a mock function with given fields: guildId, members func (_m *SocketService) EmitDeleteGuild(guildId string, members []string) { _m.Called(guildId, members) } // EmitDeleteMessage provides a mock function with given fields: room, messageId func (_m *SocketService) EmitDeleteMessage(room string, messageId string) { _m.Called(room, messageId) } // EmitEditChannel provides a mock function with given fields: room, channel func (_m *SocketService) EmitEditChannel(room string, channel *model.ChannelResponse) { _m.Called(room, channel) } // EmitEditGuild provides a mock function with given fields: guild func (_m *SocketService) EmitEditGuild(guild *model.Guild) { _m.Called(guild) } // EmitEditMessage provides a mock function with given fields: room, message func (_m *SocketService) EmitEditMessage(room string, message *model.MessageResponse) { _m.Called(room, message) } // EmitNewChannel provides a mock function with given fields: room, channel func (_m *SocketService) EmitNewChannel(room string, channel *model.ChannelResponse) { _m.Called(room, channel) } // EmitNewDMNotification provides a mock function with given fields: channelId, user func (_m *SocketService) EmitNewDMNotification(channelId string, user *model.User) { _m.Called(channelId, user) } // EmitNewMessage provides a mock function with given fields: room, message func (_m *SocketService) EmitNewMessage(room string, message *model.MessageResponse) { _m.Called(room, message) } // EmitNewNotification provides a mock function with given fields: guildId, channelId func (_m *SocketService) EmitNewNotification(guildId string, channelId string) { _m.Called(guildId, channelId) } // EmitNewPrivateChannel provides a mock function with given fields: members, channel func (_m *SocketService) EmitNewPrivateChannel(members []string, channel *model.ChannelResponse) { _m.Called(members, channel) } // EmitRemoveFriend provides a mock function with given fields: userId, memberId func (_m *SocketService) EmitRemoveFriend(userId string, memberId string) { _m.Called(userId, memberId) } // EmitRemoveFromGuild provides a mock function with given fields: memberId, guildId func (_m *SocketService) EmitRemoveFromGuild(memberId string, guildId string) { _m.Called(memberId, guildId) } // EmitRemoveMember provides a mock function with given fields: room, memberId func (_m *SocketService) EmitRemoveMember(room string, memberId string) { _m.Called(room, memberId) } // EmitSendRequest provides a mock function with given fields: room func (_m *SocketService) EmitSendRequest(room string) { _m.Called(room) } // NewSocketService creates a new instance of SocketService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewSocketService(t testing.TB) *SocketService { mock := &SocketService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/UserRepository.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" testing "testing" ) // UserRepository is an autogenerated mock type for the UserRepository type type UserRepository struct { mock.Mock } // Create provides a mock function with given fields: user func (_m *UserRepository) Create(user *model.User) (*model.User, error) { ret := _m.Called(user) var r0 *model.User if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok { r0 = rf(user) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(*model.User) error); ok { r1 = rf(user) } else { r1 = ret.Error(1) } return r0, r1 } // FindByEmail provides a mock function with given fields: email func (_m *UserRepository) FindByEmail(email string) (*model.User, error) { ret := _m.Called(email) var r0 *model.User if rf, ok := ret.Get(0).(func(string) *model.User); ok { r0 = rf(email) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(email) } else { r1 = ret.Error(1) } return r0, r1 } // FindByID provides a mock function with given fields: id func (_m *UserRepository) FindByID(id string) (*model.User, error) { ret := _m.Called(id) var r0 *model.User if rf, ok := ret.Get(0).(func(string) *model.User); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // GetFriendAndGuildIds provides a mock function with given fields: userId func (_m *UserRepository) GetFriendAndGuildIds(userId string) (*[]string, error) { ret := _m.Called(userId) var r0 *[]string if rf, ok := ret.Get(0).(func(string) *[]string); ok { r0 = rf(userId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]string) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(userId) } else { r1 = ret.Error(1) } return r0, r1 } // GetRequestCount provides a mock function with given fields: userId func (_m *UserRepository) GetRequestCount(userId string) (*int64, error) { ret := _m.Called(userId) var r0 *int64 if rf, ok := ret.Get(0).(func(string) *int64); ok { r0 = rf(userId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*int64) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(userId) } else { r1 = ret.Error(1) } return r0, r1 } // Update provides a mock function with given fields: user func (_m *UserRepository) Update(user *model.User) error { ret := _m.Called(user) var r0 error if rf, ok := ret.Get(0).(func(*model.User) error); ok { r0 = rf(user) } else { r0 = ret.Error(0) } return r0 } // NewUserRepository creates a new instance of UserRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewUserRepository(t testing.TB) *UserRepository { mock := &UserRepository{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/mocks/UserService.go ================================================ // Code generated by mockery v2.12.1. DO NOT EDIT. package mocks import ( context "context" model "github.com/sentrionic/valkyrie/model" mock "github.com/stretchr/testify/mock" multipart "mime/multipart" testing "testing" ) // UserService is an autogenerated mock type for the UserService type type UserService struct { mock.Mock } // ChangeAvatar provides a mock function with given fields: header, directory func (_m *UserService) ChangeAvatar(header *multipart.FileHeader, directory string) (string, error) { ret := _m.Called(header, directory) var r0 string if rf, ok := ret.Get(0).(func(*multipart.FileHeader, string) string); ok { r0 = rf(header, directory) } else { r0 = ret.Get(0).(string) } var r1 error if rf, ok := ret.Get(1).(func(*multipart.FileHeader, string) error); ok { r1 = rf(header, directory) } else { r1 = ret.Error(1) } return r0, r1 } // ChangePassword provides a mock function with given fields: currentPassword, newPassword, user func (_m *UserService) ChangePassword(currentPassword string, newPassword string, user *model.User) error { ret := _m.Called(currentPassword, newPassword, user) var r0 error if rf, ok := ret.Get(0).(func(string, string, *model.User) error); ok { r0 = rf(currentPassword, newPassword, user) } else { r0 = ret.Error(0) } return r0 } // DeleteImage provides a mock function with given fields: key func (_m *UserService) DeleteImage(key string) error { ret := _m.Called(key) var r0 error if rf, ok := ret.Get(0).(func(string) error); ok { r0 = rf(key) } else { r0 = ret.Error(0) } return r0 } // ForgotPassword provides a mock function with given fields: ctx, user func (_m *UserService) ForgotPassword(ctx context.Context, user *model.User) error { ret := _m.Called(ctx, user) var r0 error if rf, ok := ret.Get(0).(func(context.Context, *model.User) error); ok { r0 = rf(ctx, user) } else { r0 = ret.Error(0) } return r0 } // Get provides a mock function with given fields: id func (_m *UserService) Get(id string) (*model.User, error) { ret := _m.Called(id) var r0 *model.User if rf, ok := ret.Get(0).(func(string) *model.User); ok { r0 = rf(id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { r1 = ret.Error(1) } return r0, r1 } // GetByEmail provides a mock function with given fields: email func (_m *UserService) GetByEmail(email string) (*model.User, error) { ret := _m.Called(email) var r0 *model.User if rf, ok := ret.Get(0).(func(string) *model.User); ok { r0 = rf(email) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(email) } else { r1 = ret.Error(1) } return r0, r1 } // GetFriendAndGuildIds provides a mock function with given fields: userId func (_m *UserService) GetFriendAndGuildIds(userId string) (*[]string, error) { ret := _m.Called(userId) var r0 *[]string if rf, ok := ret.Get(0).(func(string) *[]string); ok { r0 = rf(userId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*[]string) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(userId) } else { r1 = ret.Error(1) } return r0, r1 } // GetRequestCount provides a mock function with given fields: userId func (_m *UserService) GetRequestCount(userId string) (*int64, error) { ret := _m.Called(userId) var r0 *int64 if rf, ok := ret.Get(0).(func(string) *int64); ok { r0 = rf(userId) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*int64) } } var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(userId) } else { r1 = ret.Error(1) } return r0, r1 } // IsEmailAlreadyInUse provides a mock function with given fields: email func (_m *UserService) IsEmailAlreadyInUse(email string) bool { ret := _m.Called(email) var r0 bool if rf, ok := ret.Get(0).(func(string) bool); ok { r0 = rf(email) } else { r0 = ret.Get(0).(bool) } return r0 } // Login provides a mock function with given fields: email, password func (_m *UserService) Login(email string, password string) (*model.User, error) { ret := _m.Called(email, password) var r0 *model.User if rf, ok := ret.Get(0).(func(string, string) *model.User); ok { r0 = rf(email, password) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(string, string) error); ok { r1 = rf(email, password) } else { r1 = ret.Error(1) } return r0, r1 } // Register provides a mock function with given fields: user func (_m *UserService) Register(user *model.User) (*model.User, error) { ret := _m.Called(user) var r0 *model.User if rf, ok := ret.Get(0).(func(*model.User) *model.User); ok { r0 = rf(user) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(*model.User) error); ok { r1 = rf(user) } else { r1 = ret.Error(1) } return r0, r1 } // ResetPassword provides a mock function with given fields: ctx, password, token func (_m *UserService) ResetPassword(ctx context.Context, password string, token string) (*model.User, error) { ret := _m.Called(ctx, password, token) var r0 *model.User if rf, ok := ret.Get(0).(func(context.Context, string, string) *model.User); ok { r0 = rf(ctx, password, token) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*model.User) } } var r1 error if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { r1 = rf(ctx, password, token) } else { r1 = ret.Error(1) } return r0, r1 } // UpdateAccount provides a mock function with given fields: user func (_m *UserService) UpdateAccount(user *model.User) error { ret := _m.Called(user) var r0 error if rf, ok := ret.Get(0).(func(*model.User) error); ok { r0 = rf(user) } else { r0 = ret.Error(0) } return r0 } // NewUserService creates a new instance of UserService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. func NewUserService(t testing.TB) *UserService { mock := &UserService{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) return mock } ================================================ FILE: server/model/app_constants.go ================================================ package model // Application Constants const ( MinimumChannels = 1 MaximumChannels = 50 MaximumGuilds = 100 CookieName = "vlk" ) ================================================ FILE: server/model/apperrors/apperrors.go ================================================ package apperrors // Guild Errors const ( NotAMember = "Not a member of the guild" AlreadyMember = "Already a member of the guild" GuildLimitReached = "The guild limit is 100" MustBeOwner = "Must be the owner for that" InvalidImageType = "imageFile must be 'image/jpeg' or 'image/png'" MustBeMemberInvite = "Must be a member to fetch an invite" IsPermanentError = "isPermanent is not a boolean" InvalidateInvitesError = "Only the owner can invalidate invites" InvalidInviteError = "Invalid Link or the server got deleted" BannedFromServer = "You are banned from this server" DeleteGuildError = "Only the owner can delete their server" OwnerCantLeave = "The owner cannot leave their server" BanYourselfError = "You cannot ban yourself" KickYourselfError = "You cannot kick yourself" UnbanYourselfError = "You cannot unban yourself" OneChannelRequired = "A server needs at least one channel" ChannelLimitError = "The channel limit is 50" DMYourselfError = "You cannot dm yourself" ) // Account Errors const ( InvalidOldPassword = "Invalid old password" InvalidCredentials = "Invalid email and password combination" DuplicateEmail = "An account with that email already exists" PasswordsDoNotMatch = "Passwords do not match" InvalidResetToken = "Invalid reset token" ) // Friend Errors const ( AddYourselfError = "You cannot add yourself" RemoveYourselfError = "You cannot remove yourself" AcceptYourselfError = "You cannot accept yourself" CancelYourselfError = "You cannot cancel yourself" UnableAddError = "Unable to add user as friend. Try again later" UnableRemoveError = "Unable to remove the user. Try again later" UnableAcceptError = "Unable to accept the request. Try again later" ) // Generic Errors const ( InvalidSession = "Provided session is invalid" ServerError = "Something went wrong. Try again later" Unauthorized = "Not Authorized" ) // Message Errors const ( MessageOrFileRequired = "Either a message or a file is required" EditMessageError = "Only the author can edit the message" DeleteMessageError = "Only the author or owner can delete the message" DeleteDMMessageError = "Only the author can delete the message" ) ================================================ FILE: server/model/apperrors/httperrors.go ================================================ package apperrors import ( "errors" "fmt" "net/http" ) // Type holds a type string and integer code for the error type Type string // "Set" of valid errorTypes const ( Authorization Type = "AUTHORIZATION" // Authentication Failures - BadRequest Type = "BADREQUEST" // Validation errors / BadInput Conflict Type = "CONFLICT" // Already exists (eg, create account with existent email) - 409 Internal Type = "INTERNAL" // Server (500) and fallback errors NotFound Type = "NOTFOUND" // For not finding resource PayloadTooLarge Type = "PAYLOADTOOLARGE" // for uploading tons of JSON, or an image over the limit - 413 ServiceUnavailable Type = "SERVICE_UNAVAILABLE" // For long running handlers UnsupportedMediaType Type = "UNSUPPORTEDMEDIATYPE" // for http 415 ) // Error holds a custom error for the application // which is helpful in returning a consistent // error type/message from API endpoints type Error struct { Type Type `json:"type"` Message string `json:"message"` } // Error satisfies standard error interface // we can return errors from this package as // a regular old go _error_ func (e *Error) Error() string { return e.Message } // Status is a mapping errors to status codes // Of course, this is somewhat redundant since // our errors already map http status codes func (e *Error) Status() int { switch e.Type { case Authorization: return http.StatusUnauthorized case BadRequest: return http.StatusBadRequest case Conflict: return http.StatusConflict case Internal: return http.StatusInternalServerError case NotFound: return http.StatusNotFound case PayloadTooLarge: return http.StatusRequestEntityTooLarge case ServiceUnavailable: return http.StatusServiceUnavailable case UnsupportedMediaType: return http.StatusUnsupportedMediaType default: return http.StatusInternalServerError } } // Status checks the runtime type // of the error and returns an http // status code if the error is model.Error func Status(err error) int { var e *Error if errors.As(err, &e) { return e.Status() } return http.StatusInternalServerError } /* * Error "Factories" */ // NewAuthorization to create a 401 func NewAuthorization(reason string) *Error { return &Error{ Type: Authorization, Message: reason, } } // NewBadRequest to create 400 errors (validation, for example) func NewBadRequest(reason string) *Error { return &Error{ Type: BadRequest, Message: fmt.Sprintf("Bad request. Reason: %v", reason), } } // NewConflict to create an error for 409 func NewConflict(name string, value string) *Error { return &Error{ Type: Conflict, Message: fmt.Sprintf("resource: %v with value: %v already exists", name, value), } } // NewInternal for 500 errors and unknown errors func NewInternal() *Error { return &Error{ Type: Internal, Message: ServerError, } } // NewNotFound to create an error for 404 with a generic error message func NewNotFound(name string, value string) *Error { return &Error{ Type: NotFound, Message: fmt.Sprintf("resource: %v with value: %v not found", name, value), } } // NewPayloadTooLarge to create an error for 413 func NewPayloadTooLarge(maxBodySize int64, contentLength int64) *Error { return &Error{ Type: PayloadTooLarge, Message: fmt.Sprintf("Max payload size of %v exceeded. Actual payload size: %v", maxBodySize, contentLength), } } // NewServiceUnavailable to create an error for 503 func NewServiceUnavailable() *Error { return &Error{ Type: ServiceUnavailable, Message: "Service unavailable or timed out", } } // NewUnsupportedMediaType to create an error for 415 func NewUnsupportedMediaType(reason string) *Error { return &Error{ Type: UnsupportedMediaType, Message: reason, } } ================================================ FILE: server/model/base_model.go ================================================ package model import ( "time" ) // BaseModel is similar to the gorm.Model and it includes the // ID as a string, CreatedAt and UpdatedAt fields type BaseModel struct { ID string `gorm:"primaryKey" json:"id"` CreatedAt time.Time `gorm:"index" json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } // Success is the default response for successful operation // Returns true without the JSON type Success struct { // Only returns true, not a json object Success bool `json:"success"` } //@name SuccessResponse ================================================ FILE: server/model/channel.go ================================================ package model import "time" // Channel represents a text channel in a guild // or a text channel for DMs between users. // GuildID should only be nil if it is a DM channel // PCMembers should only be used if the channel is private. type Channel struct { BaseModel GuildID *string `gorm:"index"` Name string `gorm:"name"` IsPublic bool `gorm:"index"` IsDM bool `gorm:"is_dm"` LastActivity time.Time `gorm:"autoCreateTime"` PCMembers []User `gorm:"many2many:pcmembers;constraint:OnDelete:CASCADE;"` Messages []Message `gorm:"constraint:OnDelete:CASCADE;"` } // ChannelResponse is the JSON response of the channel type ChannelResponse struct { Id string `json:"id"` Name string `json:"name"` IsPublic bool `json:"isPublic"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` HasNotification bool `json:"hasNotification"` } //@name Channel // SerializeChannel returns the channel API response. func (c Channel) SerializeChannel() ChannelResponse { return ChannelResponse{ Id: c.ID, Name: c.Name, IsPublic: c.IsPublic, CreatedAt: c.CreatedAt, UpdatedAt: c.UpdatedAt, HasNotification: false, } } // ChannelService defines methods related to channel operations the handler layer expects // any service it interacts with to implement type ChannelService interface { CreateChannel(channel *Channel) (*Channel, error) GetChannels(userId string, guildId string) (*[]ChannelResponse, error) Get(channelId string) (*Channel, error) GetPrivateChannelMembers(channelId string) (*[]string, error) GetDirectMessages(userId string) (*[]DirectMessage, error) GetDirectMessageChannel(userId string, memberId string) (*string, error) GetDMByUserAndChannel(userId string, channelId string) (string, error) AddDMChannelMembers(memberIds []string, channelId string, userId string) error SetDirectMessageStatus(dmId string, userId string, isOpen bool) error DeleteChannel(channel *Channel) error UpdateChannel(channel *Channel) error CleanPCMembers(channelId string) error AddPrivateChannelMembers(memberIds []string, channelId string) error RemovePrivateChannelMembers(memberIds []string, channelId string) error IsChannelMember(channel *Channel, userId string) error OpenDMForAll(dmId string) error } // ChannelRepository defines methods related to channel db operations the service layer expects // any repository it interacts with to implement type ChannelRepository interface { Create(channel *Channel) (*Channel, error) GetGuildDefault(guildId string) (*Channel, error) Get(userId string, guildId string) (*[]ChannelResponse, error) GetDirectMessages(userId string) (*[]DirectMessage, error) GetDirectMessageChannel(userId string, memberId string) (*string, error) GetById(channelId string) (*Channel, error) GetPrivateChannelMembers(channelId string) (*[]string, error) AddDMChannelMembers(members []DMMember) error SetDirectMessageStatus(dmId string, userId string, isOpen bool) error DeleteChannel(channel *Channel) error UpdateChannel(channel *Channel) error CleanPCMembers(channelId string) error AddPrivateChannelMembers(memberIds []string, channelId string) error RemovePrivateChannelMembers(memberIds []string, channelId string) error FindDMByUserAndChannelId(channelId, userId string) (string, error) OpenDMForAll(dmId string) error GetDMMemberIds(channelId string) (*[]string, error) } ================================================ FILE: server/model/direct_message.go ================================================ package model // DirectMessage is the json response of the channel ID // and the other user of the DM. type DirectMessage struct { Id string `json:"id"` User DMUser `json:"user"` } //@name DirectMessage // DMUser is the other member of the DM. type DMUser struct { Id string `json:"id"` Username string `json:"username"` Image string `json:"image"` IsOnline bool `json:"isOnline"` IsFriend bool `json:"isFriend"` } //@name DMUser ================================================ FILE: server/model/dm_member.go ================================================ package model import "time" // DMMember represents a member of a DM channel. // IsOpen indicates if the DM is open on the client. type DMMember struct { ID string `gorm:"primaryKey"` UserID string `gorm:"primaryKey"` ChannelId string `gorm:"primaryKey;"` IsOpen bool CreatedAt time.Time `gorm:"index"` UpdatedAt time.Time } ================================================ FILE: server/model/field_error.go ================================================ package model // ErrorsResponse contains a list of FieldError type ErrorsResponse struct { Errors []FieldError `json:"errors"` } //@name ErrorsResponse // FieldError is used to help extract validation errors type FieldError struct { // The property containing the error Field string `json:"field"` // The specific error message Message string `json:"message"` } //@name FieldError // ErrorResponse holds a custom error for the application type ErrorResponse struct { Error HttpError `json:"error"` } //@name ErrorResponse // HttpError returns the Http error type and the specific message type HttpError struct { // The Http Response as a string Type string `json:"type"` // The specific error message Message string `json:"message"` } //@name HttpError ================================================ FILE: server/model/fixture/channel.go ================================================ package fixture import ( "github.com/sentrionic/valkyrie/model" "time" ) // GetMockChannel returns a mock channel. If guildId is not empty it will set GuildID to that id. func GetMockChannel(guildId string) *model.Channel { var guild *string = nil if guildId != "" { guild = &guildId } return &model.Channel{ BaseModel: model.BaseModel{ ID: RandID(), CreatedAt: time.Now(), UpdatedAt: time.Now(), }, GuildID: guild, Name: RandStr(8), IsPublic: true, LastActivity: time.Now(), } } // GetMockDMChannel returns a mock channel that has IsDM set to true and does not belong to a guild. func GetMockDMChannel() *model.Channel { return &model.Channel{ BaseModel: model.BaseModel{ ID: RandID(), CreatedAt: time.Now(), UpdatedAt: time.Now(), }, Name: RandID(), IsDM: true, LastActivity: time.Now(), } } ================================================ FILE: server/model/fixture/faker.go ================================================ package fixture import ( "crypto/md5" "encoding/hex" "fmt" "math/rand" "strings" ) var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") var numberRunes = []rune("1234567890") // RandStringRunes returns a random latin string of the given length func RandStringRunes(n int) string { b := make([]rune, n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes))] } return string(b) } func randNumberRunes(n int) string { b := make([]rune, n) for i := range b { b[i] = numberRunes[rand.Intn(len(numberRunes))] } return string(b) } func randStringLowerRunes(n int) string { b := make([]rune, n) for i := range b { b[i] = letterRunes[rand.Intn(len(letterRunes)/2)] } return string(b) } // RandInt returns a random int within the given range func RandInt(min, max int) int { return rand.Intn(max-min+1) + min } // RandID returns a 15 character long numeric string func RandID() string { return randNumberRunes(15) } // Username returns a random string that's 4 to 15 characters long func Username() string { return RandStringRunes(RandInt(4, 15)) } // Email returns a random email ending with @example.com func Email() string { email := fmt.Sprintf("%s@example.com", randStringLowerRunes(RandInt(5, 10))) return strings.ToLower(email) } // RandStr returns a random string that has the given length func RandStr(n int) string { return RandStringRunes(n) } // generateAvatar returns an gravatar using the md5 hash of the email func generateAvatar(email string) string { hash := md5.Sum([]byte(email)) return fmt.Sprintf("https://gravatar.com/avatar/%s?d=identicon", hex.EncodeToString(hash[:])) } ================================================ FILE: server/model/fixture/guild.go ================================================ package fixture import ( "github.com/sentrionic/valkyrie/model" "time" ) // GetMockGuild returns a mock guild with the given uid as the owner. func GetMockGuild(uid string) *model.Guild { ownerId := RandID() if uid != "" { ownerId = uid } return &model.Guild{ BaseModel: model.BaseModel{ ID: RandID(), CreatedAt: time.Now(), UpdatedAt: time.Now(), }, Name: RandStr(8), OwnerId: ownerId, } } ================================================ FILE: server/model/fixture/message.go ================================================ package fixture import ( "github.com/sentrionic/valkyrie/model" "time" ) // GetMockMessage returns a mock message with the given uid as the owner and the cid as the ChannelId func GetMockMessage(uid, cid string) *model.Message { text := RandStringRunes(100) ownerId := RandID() if uid != "" { ownerId = uid } return &model.Message{ BaseModel: model.BaseModel{ ID: RandID(), CreatedAt: time.Now(), UpdatedAt: time.Now(), }, Text: &text, UserId: ownerId, ChannelId: cid, Attachment: nil, } } // GetMockMessageResponse returns a mock message response with the given uid as the owner and the cid as the ChannelId func GetMockMessageResponse(uid, cid string) *model.MessageResponse { message := GetMockMessage(uid, cid) user := GetMockUser() return &model.MessageResponse{ Id: message.ID, Text: message.Text, CreatedAt: message.CreatedAt, UpdatedAt: message.UpdatedAt, Attachment: message.Attachment, User: model.MemberResponse{ Id: user.ID, Username: user.Username, Image: user.Image, IsOnline: user.IsOnline, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, Nickname: nil, Color: nil, IsFriend: false, }, } } ================================================ FILE: server/model/fixture/multipart.go ================================================ package fixture import ( "bytes" "fmt" "image" "image/png" "io" "mime" "mime/multipart" "net/textproto" "os" "path/filepath" "runtime" ) // MultipartImage used for instantiating a test fixture // for creating multipart file uploads containing an image type MultipartImage struct { imagePath string ImageFile *os.File MultipartBody *bytes.Buffer ContentType string } // NewMultipartImage creates an image file for testing // and creates a Multipart Form with this image file // for testing func NewMultipartImage(fileName string, contentType string) *MultipartImage { // create test file in same folder as this fixture _, b, _, _ := runtime.Caller(0) dir := filepath.Dir(b) imagePath := filepath.Join(dir, fileName) // f, _ := os.Open(imagePath) f := createImage(imagePath) defer func(f *os.File) { _ = f.Close() }(f) // create a multipart write onto which we // will write the image file body := &bytes.Buffer{} writer := multipart.NewWriter(body) // manually create form file as CreateFormFile will // force file's content type to "application/octet-stream" h := make(textproto.MIMEHeader) h.Set( "Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, "file", fileName), ) h.Set("Content-Type", contentType) part, _ := writer.CreatePart(h) _, _ = io.Copy(part, f) _ = writer.Close() return &MultipartImage{ imagePath: imagePath, ImageFile: f, MultipartBody: body, ContentType: writer.FormDataContentType(), } } // GetFormFile extracts form file from multipart body func (m *MultipartImage) GetFormFile() *multipart.FileHeader { _, params, _ := mime.ParseMediaType(m.ContentType) mr := multipart.NewReader(m.MultipartBody, params["boundary"]) form, _ := mr.ReadForm(1024) files := form.File["file"] return files[0] } // Close removes created file for test func (m *MultipartImage) Close() { _ = m.ImageFile.Close() _ = os.Remove(m.imagePath) } // createImage used to create a quick example // 1px x 1px image encoded as a PNG func createImage(imagePath string) *os.File { rect := image.Rect(0, 0, 1, 1) img := image.NewRGBA(rect) f, _ := os.Create(imagePath) _ = png.Encode(f, img) return f } ================================================ FILE: server/model/fixture/user.go ================================================ package fixture import ( "github.com/sentrionic/valkyrie/model" "time" ) // GetMockUser returns a mock user func GetMockUser() *model.User { email := Email() return &model.User{ BaseModel: model.BaseModel{ ID: RandID(), CreatedAt: time.Now(), UpdatedAt: time.Now(), }, Username: Username(), Email: email, Password: RandStr(8), Image: generateAvatar(email), } } ================================================ FILE: server/model/friend.go ================================================ package model // Friend represents the api response of a user's friend. type Friend struct { Id string `json:"id"` Username string `json:"username"` Image string `json:"image"` IsOnline bool `json:"isOnline"` } //@name Friend // FriendService defines methods related to friend operations the handler layer expects // any service it interacts with to implement type FriendService interface { GetFriends(id string) (*[]Friend, error) GetRequests(id string) (*[]FriendRequest, error) GetMemberById(id string) (*User, error) DeleteRequest(memberId string, userId string) error RemoveFriend(memberId string, userId string) error SaveRequests(user *User) error } // FriendRepository defines methods related to friend db operations the service layer expects // any repository it interacts with to implement type FriendRepository interface { FindByID(id string) (*User, error) FriendsList(id string) (*[]Friend, error) RequestList(id string) (*[]FriendRequest, error) DeleteRequest(memberId string, userId string) error RemoveFriend(memberId string, userId string) error Save(user *User) error } ================================================ FILE: server/model/friend_request.go ================================================ package model // RequestType stands for the type of friend request type RequestType int // FriendRequest RequestType enum const ( Outgoing RequestType = iota Incoming ) // FriendRequest contains all info to display request. // Type stands for the type of the request. // 1: Incoming, // 0: Outgoing type FriendRequest struct { Id string `json:"id"` Username string `json:"username"` Image string `json:"image"` // 1: Incoming, 0: Outgoing Type RequestType `json:"type" enums:"0,1"` } //@name FriendRequest ================================================ FILE: server/model/guild.go ================================================ package model import ( "context" "github.com/lib/pq" "time" ) // Guild represents the server many users can chat in. type Guild struct { BaseModel Name string `gorm:"not null"` OwnerId string `gorm:"not null"` Icon *string InviteLinks pq.StringArray `gorm:"type:text[]"` Members []User `gorm:"many2many:members;constraint:OnDelete:CASCADE;"` Channels []Channel `gorm:"constraint:OnDelete:CASCADE;"` Bans []User `gorm:"many2many:bans;constraint:OnDelete:CASCADE;"` VCMembers []User `gorm:"many2many:vc_members;constraint:OnDelete:CASCADE;"` } // GuildResponse contains all info to display a guild. // The DefaultChannelId is the channel the user first gets directed to // and is the oldest channel of the guild. type GuildResponse struct { Id string `json:"id"` Name string `json:"name"` OwnerId string `json:"ownerId"` Icon *string `json:"icon"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` HasNotification bool `json:"hasNotification"` DefaultChannelId string `json:"default_channel_id"` } //@name GuildResponse // SerializeGuild returns the guild API response. // The DefaultChannelId represents the default channel the user gets send to. func (g Guild) SerializeGuild(channelId string) GuildResponse { return GuildResponse{ Id: g.ID, Name: g.Name, OwnerId: g.OwnerId, Icon: g.Icon, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, HasNotification: false, DefaultChannelId: channelId, } } // GuildService defines methods related to guild operations the handler layer expects // any service it interacts with to implement type GuildService interface { GetUser(uid string) (*User, error) GetGuild(id string) (*Guild, error) GetUserGuilds(uid string) (*[]GuildResponse, error) GetGuildMembers(userId string, guildId string) (*[]MemberResponse, error) GetVCMembers(guildId string) (*[]VCMemberResponse, error) CreateGuild(guild *Guild) (*Guild, error) GenerateInviteLink(ctx context.Context, guildId string, isPermanent bool) (string, error) UpdateGuild(guild *Guild) error GetGuildIdFromInvite(ctx context.Context, token string) (string, error) GetDefaultChannel(guildId string) (*Channel, error) InvalidateInvites(ctx context.Context, guild *Guild) RemoveMember(userId string, guildId string) error UnbanMember(userId string, guildId string) error DeleteGuild(guildId string) error GetBanList(guildId string) (*[]BanResponse, error) GetMemberSettings(userId string, guildId string) (*MemberSettings, error) UpdateMemberSettings(settings *MemberSettings, userId string, guildId string) error FindUsersByIds(ids []string, guildId string) (*[]User, error) UpdateMemberLastSeen(userId, guildId string) error RemoveVCMember(userId, guildId string) error UpdateVCMember(isMuted, isDeafened bool, userId, guildId string) error GetVCMember(userId, guildId string) (*VCMember, error) } // GuildRepository defines methods related to guild db operations the service layer expects // any repository it interacts with to implement type GuildRepository interface { FindUserByID(uid string) (*User, error) FindByID(id string) (*Guild, error) List(uid string) (*[]GuildResponse, error) GuildMembers(userId string, guildId string) (*[]MemberResponse, error) VCMembers(guildId string) (*[]VCMemberResponse, error) Create(guild *Guild) (*Guild, error) Save(guild *Guild) error RemoveMember(userId string, guildId string) error Delete(guildId string) error UnbanMember(userId string, guildId string) error GetBanList(guildId string) (*[]BanResponse, error) GetMemberSettings(userId string, guildId string) (*MemberSettings, error) UpdateMemberSettings(settings *MemberSettings, userId string, guildId string) error FindUsersByIds(ids []string, guildId string) (*[]User, error) GetMember(userId, guildId string) (*User, error) UpdateMemberLastSeen(userId, guildId string) error RemoveVCMember(userId, guildId string) error GetMemberIds(guildId string) (*[]string, error) UpdateVCMember(isMuted, isDeafened bool, userId, guildId string) error GetVCMember(userId, guildId string) (*VCMember, error) } ================================================ FILE: server/model/interfaces.go ================================================ package model import ( "context" "mime/multipart" ) // FileRepository defines methods related to file upload the service layer expects // any repository it interacts with to implement type FileRepository interface { UploadAvatar(header *multipart.FileHeader, directory string) (string, error) UploadFile(header *multipart.FileHeader, directory, filename, mimetype string) (string, error) DeleteImage(key string) error } // MailRepository defines methods related to mail operations the service layer expects // any repository it interacts with to implement type MailRepository interface { SendResetMail(email string, html string) error } // RedisRepository defines methods related to the redis db the service layer expects // any repository it interacts with to implement type RedisRepository interface { SetResetToken(ctx context.Context, id string) (string, error) GetIdFromToken(ctx context.Context, token string) (string, error) SaveInvite(ctx context.Context, guildId string, id string, isPermanent bool) error GetInvite(ctx context.Context, token string) (string, error) InvalidateInvites(ctx context.Context, guild *Guild) } ================================================ FILE: server/model/invite.go ================================================ package model // Invite represents an invite link for a guild // IsPermanent indicates if the invite should not expire type Invite struct { GuildId string `json:"guild_id"` IsPermanent bool `json:"is_permanent"` } ================================================ FILE: server/model/member.go ================================================ package model import "time" // Member represents a user in a guild and is the join table between // User and Guild. type Member struct { UserID string `gorm:"primaryKey;constraint:OnDelete:CASCADE;"` GuildID string `gorm:"primaryKey;constraint:OnDelete:CASCADE;"` Nickname *string `gorm:"nickname"` Color *string `gorm:"color"` LastSeen time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"index"` UpdatedAt time.Time } type VCMember struct { UserID string `gorm:"primaryKey;constraint:OnDelete:CASCADE;"` GuildID string `gorm:"primaryKey;constraint:OnDelete:CASCADE;"` IsMuted bool IsDeafened bool } // MemberResponse is the API response of a member. type MemberResponse struct { Id string `json:"id"` Username string `json:"username"` Image string `json:"image"` IsOnline bool `json:"isOnline"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` Nickname *string `json:"nickname"` Color *string `json:"color"` IsFriend bool `json:"isFriend"` } //@name Member // BanResponse is the API response of a banned member. type BanResponse struct { Id string `json:"id"` Username string `json:"username"` Image string `json:"image"` } //@name BanResponse // MemberSettings is the API response of a member's guild settings. type MemberSettings struct { Nickname *string `json:"nickname"` Color *string `json:"color"` } //@name MemberSettings // VCMemberResponse is the API response of a member that is currently in a VC. type VCMemberResponse struct { Id string `json:"id"` Username string `json:"username"` Image string `json:"image"` IsMuted bool `json:"isMuted"` IsDeafened bool `json:"IsDeafened"` Nickname *string `json:"nickname"` } //@name VCMember ================================================ FILE: server/model/message.go ================================================ package model import ( "mime/multipart" "time" ) // Message represents a text message in a channel. // It may contain an Attachment that is displayed instead of text. type Message struct { BaseModel Text *string UserId string `gorm:"index;constraint:OnDelete:CASCADE;"` ChannelId string `gorm:"index;constraint:OnDelete:CASCADE;"` Attachment *Attachment `gorm:"constraint:OnDelete:CASCADE;"` } // MessageResponse is the API response of a Message type MessageResponse struct { Id string `json:"id"` Text *string `json:"text"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` Attachment *Attachment `json:"attachment"` User MemberResponse `json:"user"` } //@name Message // Attachment represents a message attachment that displays // a file instead of text. type Attachment struct { ID string `gorm:"primaryKey" json:"-"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` Url string `json:"url"` FileType string `json:"filetype"` Filename string `json:"filename"` MessageId string `gorm:"index;constraint:OnDelete:CASCADE;" json:"-"` } //@name Attachment // MessageService defines methods related to message operations the handler layer expects // any service it interacts with to implement type MessageService interface { GetMessages(userId string, channel *Channel, cursor string) (*[]MessageResponse, error) CreateMessage(params *Message) (*Message, error) UpdateMessage(message *Message) error DeleteMessage(message *Message) error UploadFile(header *multipart.FileHeader, channelId string) (*Attachment, error) Get(messageId string) (*Message, error) } // MessageRepository defines methods related message db operations the service layer expects // any repository it interacts with to implement type MessageRepository interface { GetMessages(userId string, channel *Channel, cursor string) (*[]MessageResponse, error) CreateMessage(params *Message) (*Message, error) UpdateMessage(message *Message) error DeleteMessage(message *Message) error GetById(messageId string) (*Message, error) } ================================================ FILE: server/model/user.go ================================================ package model import ( "context" "mime/multipart" ) // User represents the user of the website. type User struct { BaseModel Username string `gorm:"not null" json:"username"` Email string `gorm:"not null;uniqueIndex" json:"email"` Password string `gorm:"not null" json:"-"` Image string `json:"image"` IsOnline bool `gorm:"index;default:true" json:"isOnline"` Friends []User `gorm:"many2many:friends;" json:"-"` Requests []User `gorm:"many2many:friend_requests;joinForeignKey:sender_id;joinReferences:receiver_id" json:"-"` Guilds []Guild `gorm:"many2many:members;" json:"-"` Message []Message `json:"-"` } //@name User // UserService defines methods related to account operations the handler layer expects // any service it interacts with to implement type UserService interface { Get(id string) (*User, error) GetByEmail(email string) (*User, error) Register(user *User) (*User, error) Login(email, password string) (*User, error) UpdateAccount(user *User) error IsEmailAlreadyInUse(email string) bool ChangeAvatar(header *multipart.FileHeader, directory string) (string, error) DeleteImage(key string) error ChangePassword(currentPassword, newPassword string, user *User) error ForgotPassword(ctx context.Context, user *User) error ResetPassword(ctx context.Context, password string, token string) (*User, error) GetFriendAndGuildIds(userId string) (*[]string, error) GetRequestCount(userId string) (*int64, error) } // UserRepository defines methods related to account db operations the service layer expects // any repository it interacts with to implement type UserRepository interface { FindByID(id string) (*User, error) Create(user *User) (*User, error) FindByEmail(email string) (*User, error) Update(user *User) error GetFriendAndGuildIds(userId string) (*[]string, error) GetRequestCount(userId string) (*int64, error) } ================================================ FILE: server/model/ws_message.go ================================================ package model import ( "encoding/json" "log" ) // ReceivedMessage represents a received websocket message type ReceivedMessage struct { Action string `json:"action"` Room string `json:"room"` Message *any `json:"message"` } // WebsocketMessage represents an emitted message type WebsocketMessage struct { Action string `json:"action"` Data any `json:"data"` } // Encode turns the message into a byte array func (message *WebsocketMessage) Encode() []byte { encoding, err := json.Marshal(message) if err != nil { log.Println(err) } return encoding } // SocketService defines methods related emitting websockets events the service layer expects // any repository it interacts with to implement type SocketService interface { EmitNewMessage(room string, message *MessageResponse) EmitEditMessage(room string, message *MessageResponse) EmitDeleteMessage(room, messageId string) EmitNewChannel(room string, channel *ChannelResponse) EmitNewPrivateChannel(members []string, channel *ChannelResponse) EmitEditChannel(room string, channel *ChannelResponse) EmitDeleteChannel(channel *Channel) EmitEditGuild(guild *Guild) EmitDeleteGuild(guildId string, members []string) EmitRemoveFromGuild(memberId, guildId string) EmitAddMember(room string, member *User) EmitRemoveMember(room, memberId string) EmitNewDMNotification(channelId string, user *User) EmitNewNotification(guildId, channelId string) EmitSendRequest(room string) EmitAddFriendRequest(room string, request *FriendRequest) EmitAddFriend(user, member *User) EmitRemoveFriend(userId, memberId string) } ================================================ FILE: server/repository/channel_repository.go ================================================ package repository import ( "database/sql" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "gorm.io/gorm" "log" "time" ) // channelRepository is data/repository implementation // of service layer ChannelRepository type channelRepository struct { DB *gorm.DB } // NewChannelRepository is a factory for initializing Channel Repositories func NewChannelRepository(db *gorm.DB) model.ChannelRepository { return &channelRepository{ DB: db, } } // Create inserts a channel in the DB func (r *channelRepository) Create(channel *model.Channel) (*model.Channel, error) { if result := r.DB.Create(&channel); result.Error != nil { log.Printf("Could not create a channel for guild: %v. Reason: %v\n", channel.GuildID, result.Error) return nil, apperrors.NewInternal() } return channel, nil } // GetGuildDefault fetches the oldest channel for the given guildId from the DB func (r *channelRepository) GetGuildDefault(guildId string) (*model.Channel, error) { channel := model.Channel{} result := r.DB. Where("guild_id = ?", guildId). Order("created_at ASC"). First(&channel) return &channel, result.Error } // Get fetches all public channels for the given guildId // and the private channels the given user is part in func (r *channelRepository) Get(userId string, guildId string) (*[]model.ChannelResponse, error) { var channels []model.ChannelResponse result := r.DB. Raw(` SELECT DISTINCT ON (c.id, c."created_at") c.id, c.name, c."is_public", c."created_at", c."updated_at", (c."last_activity" > m."last_seen") AS "hasNotification" FROM channels AS c LEFT OUTER JOIN pcmembers as pc ON c."id"::text = pc."channel_id"::text LEFT OUTER JOIN members m on c."guild_id" = m."guild_id" WHERE c."guild_id"::text = ? AND (c."is_public" = true or pc."user_id"::text = ?) ORDER BY c."created_at" `, guildId, userId). Scan(&channels) return &channels, result.Error } // dmQuery represents the fetched fields for GetDirectMessages type dmQuery struct { ChannelId string Id string Username string Image string IsOnline bool IsFriend bool } // GetDirectMessages returns all DMs for the given user func (r *channelRepository) GetDirectMessages(userId string) (*[]model.DirectMessage, error) { var results []dmQuery err := r.DB. Raw(` SELECT dm."channel_id", u.username, u.image, u.id, u."is_online", u."created_at", u."updated_at" FROM users u JOIN dm_members dm ON dm."user_id" = u.id WHERE u.id != @id AND dm."channel_id" IN ( SELECT DISTINCT c.id FROM channels as c LEFT OUTER JOIN dm_members as dm ON c."id" = dm."channel_id" JOIN users u on dm."user_id" = u.id WHERE c."is_public" = false AND c.is_dm = true AND dm."is_open" = true AND dm."user_id" = @id ) order by dm."updated_at" DESC `, sql.Named("id", userId)). Scan(&results) var channels []model.DirectMessage // Turn into DirectMessage response for _, dm := range results { channel := model.DirectMessage{ Id: dm.ChannelId, User: model.DMUser{ Id: dm.Id, Username: dm.Username, Image: dm.Image, IsOnline: dm.IsOnline, IsFriend: dm.IsFriend, }, } channels = append(channels, channel) } return &channels, err.Error } // GetDirectMessageChannel returns the dm channel ID of the given members // if it exists. func (r *channelRepository) GetDirectMessageChannel(userId string, memberId string) (*string, error) { var id string result := r.DB. Raw(` SELECT c.id FROM channels as c, dm_members dm WHERE dm."channel_id" = c."id" AND c.is_dm = true AND c."is_public" = false GROUP BY c."id" HAVING array_agg(dm."user_id"::text) @> Array[?,?] AND count(dm."user_id") = 2; `, userId, memberId). Scan(&id) return &id, result.Error } // GetById returns the channel with its PCMembers for the given channel id func (r *channelRepository) GetById(channelId string) (*model.Channel, error) { var channel model.Channel err := r.DB.Preload("PCMembers").Where("id = ?", channelId).First(&channel).Error return &channel, err } // GetPrivateChannelMembers returns the ids of all users // that are members of the given channel func (r *channelRepository) GetPrivateChannelMembers(channelId string) (*[]string, error) { var members []string err := r.DB. Raw("SELECT pc.user_id FROM pcmembers pc JOIN channels c ON pc.channel_id = c.id WHERE c.id = ?", channelId). Scan(&members).Error return &members, err } // AddDMChannelMembers inserts the given DM members in the DB func (r *channelRepository) AddDMChannelMembers(members []model.DMMember) error { if err := r.DB.CreateInBatches(&members, len(members)).Error; err != nil { log.Printf("Could not add members to DM. Reason: %v\n", err) return apperrors.NewInternal() } return nil } // SetDirectMessageStatus opens or closes the dm channel for the given // userId func (r *channelRepository) SetDirectMessageStatus(dmId string, userId string, isOpen bool) error { err := r.DB. Table("dm_members"). Where("channel_id = ? AND user_id = ?", dmId, userId). Updates(map[string]any{ "is_open": isOpen, "updated_at": time.Now(), }). Error return err } // OpenDMForAll opens the given dm channel for all users in the channel func (r *channelRepository) OpenDMForAll(dmId string) error { err := r.DB. Table("dm_members"). Where("channel_id = ? ", dmId). Updates(map[string]any{ "is_open": true, "updated_at": time.Now(), }). Error return err } // DeleteChannel deletes the given channel from the DB func (r *channelRepository) DeleteChannel(channel *model.Channel) error { if result := r.DB.Delete(&channel); result.Error != nil { log.Printf("Could not delete the channel with id: %v. Reason: %v\n", channel, result.Error) return apperrors.NewInternal() } return nil } // UpdateChannel updates the given channel in the DB func (r *channelRepository) UpdateChannel(channel *model.Channel) error { if result := r.DB.Save(&channel); result.Error != nil { log.Printf("Could not update the given channel: %v. Reason: %v\n", channel.ID, result.Error) return apperrors.NewInternal() } return nil } // CleanPCMembers removes all private channel members from the given channel func (r *channelRepository) CleanPCMembers(channelId string) error { if result := r.DB.Exec("DELETE FROM pcmembers WHERE channel_id = ?", channelId); result.Error != nil { log.Printf("Could not clean members from the channel with id: %v. Reason: %v\n", channelId, result.Error) return apperrors.NewInternal() } return nil } // AddPrivateChannelMembers inserts the given member as PCMembers in the given channel func (r *channelRepository) AddPrivateChannelMembers(memberIds []string, channelId string) error { var err error = nil for _, id := range memberIds { err = r.DB.Exec("INSERT INTO pcmembers VALUES (?, ?)", channelId, id).Error } if err != nil { log.Printf("Could not add members to private channel %s. Reason: %v\n", channelId, err) return apperrors.NewInternal() } return nil } // RemovePrivateChannelMembers removes the given ids from the PCMembers of the given channel func (r *channelRepository) RemovePrivateChannelMembers(memberIds []string, channelId string) error { if err := r.DB.Exec("DELETE FROM pcmembers WHERE channel_id = ? AND user_id IN ?", channelId, memberIds). Error; err != nil { log.Printf("Could not remove members from private channel %s. Reason: %v\n", channelId, err) return apperrors.NewInternal() } return nil } // FindDMByUserAndChannelId returns the id of dm channel for the given channelId and userId func (r *channelRepository) FindDMByUserAndChannelId(channelId, userId string) (string, error) { var id string err := r.DB. Raw("SELECT id FROM dm_members WHERE user_id = ? AND channel_id = ?", userId, channelId). Scan(&id).Error return id, err } // GetDMMemberIds returns the ids of all dm members for the given channel func (r *channelRepository) GetDMMemberIds(channelId string) (*[]string, error) { var members []string err := r.DB. Raw("SELECT u.id FROM users u JOIN dm_members dm on u.id = dm.user_id WHERE dm.channel_id = ?", channelId). Scan(&members).Error return &members, err } ================================================ FILE: server/repository/file_repository.go ================================================ package repository import ( "bytes" "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/disintegration/imaging" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/service" "image" "image/jpeg" "log" // Register accepted file type jpeg _ "image/jpeg" // Register accepted file type png _ "image/png" "mime/multipart" ) // s3FileRepository includes the S3 session and the BucketName type s3FileRepository struct { S3Session *session.Session BucketName string } // NewFileRepository is a factory for initializing the FileRepository func NewFileRepository(session *session.Session, bucketName string) model.FileRepository { return &s3FileRepository{ S3Session: session, BucketName: bucketName, } } // UploadAvatar uploads the given image to the initialized Bucket. // The image gets resized before being uploaded. // All images turn into jpeg images. // It returns the url of the uploaded file. func (s *s3FileRepository) UploadAvatar(header *multipart.FileHeader, directory string) (string, error) { uploader := s3manager.NewUploader(s.S3Session) id := service.GenerateId() key := fmt.Sprintf("files/%s/%s.jpeg", directory, id) file, err := header.Open() if err != nil { log.Printf("Failed to open header: %v\n", err.Error()) return "", apperrors.NewInternal() } src, _, err := image.Decode(file) if err != nil { log.Printf("Failed to decode image: %v\n", err.Error()) return "", apperrors.NewInternal() } img := imaging.Resize(src, 150, 0, imaging.Lanczos) buf := new(bytes.Buffer) err = jpeg.Encode(buf, img, &jpeg.Options{Quality: 75}) if err != nil { log.Printf("Failed to encode image: %v\n", err.Error()) return "", apperrors.NewInternal() } up, err := uploader.Upload(&s3manager.UploadInput{ Body: buf, Bucket: aws.String(s.BucketName), ContentType: aws.String("image/jpeg"), Key: aws.String(key), }) if err != nil { log.Printf("Failed to upload file: %v\n", err.Error()) return "", apperrors.NewInternal() } if err = file.Close(); err != nil { log.Printf("Failed to close file: %v\n", err.Error()) return "", apperrors.NewInternal() } return up.Location, nil } // UploadFile uploads the given file to the initialized Bucket. // It returns the url of the uploaded file. func (s *s3FileRepository) UploadFile(header *multipart.FileHeader, directory, filename, mimetype string) (string, error) { uploader := s3manager.NewUploader(s.S3Session) key := fmt.Sprintf("files/%s/%s", directory, filename) file, err := header.Open() if err != nil { log.Printf("Failed to open header: %v\n", err.Error()) return "", apperrors.NewInternal() } up, err := uploader.Upload(&s3manager.UploadInput{ Body: file, Bucket: aws.String(s.BucketName), ContentType: aws.String(mimetype), Key: aws.String(key), }) if err != nil { log.Printf("Failed to upload file: %v\n", err.Error()) return "", apperrors.NewInternal() } if err = file.Close(); err != nil { log.Printf("Failed to close file: %v\n", err.Error()) return "", apperrors.NewInternal() } return up.Location, nil } // DeleteImage deletes the file from the Bucket. func (s *s3FileRepository) DeleteImage(key string) error { srv := s3.New(s.S3Session) _, err := srv.DeleteObject(&s3.DeleteObjectInput{ Bucket: aws.String(s.BucketName), Key: aws.String(key), }) if err != nil { log.Printf("Failed to delete image: %v\n", err.Error()) return apperrors.NewInternal() } return nil } ================================================ FILE: server/repository/friend_repository.go ================================================ package repository import ( "database/sql" "errors" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "gorm.io/gorm" ) // friendRepository is data/repository implementation // of service layer FriendRepository type friendRepository struct { DB *gorm.DB } // NewFriendRepository is a factory for initializing Friend Repositories func NewFriendRepository(db *gorm.DB) model.FriendRepository { return &friendRepository{ DB: db, } } // FriendsList returns the friends for the given user ID from the DB func (r *friendRepository) FriendsList(id string) (*[]model.Friend, error) { var friends []model.Friend result := r.DB. Table("users"). Joins(`JOIN friends ON friends.user_id = "users".id`). Where("friends.friend_id = ?", id). Find(&friends) return &friends, result.Error } // RequestList returns the friend requests for the given user ID from the DB func (r *friendRepository) RequestList(id string) (*[]model.FriendRequest, error) { var requests []model.FriendRequest result := r.DB. Raw(` select u.id, u.username, u.image, 1 as "type" from users u join friend_requests fr on u.id = fr."sender_id" where fr."receiver_id" = @id UNION select u.id, u.username, u.image, 0 as "type" from users u join friend_requests fr on u.id = fr."receiver_id" where fr."sender_id" = @id order by username; `, sql.Named("id", id)). Find(&requests) return &requests, result.Error } // FindByID returns a User containing their friends and requests from the DB func (r *friendRepository) FindByID(id string) (*model.User, error) { user := &model.User{} if err := r.DB. Preload("Friends"). Preload("Requests"). Where("id = ?", id). First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return user, apperrors.NewNotFound("uid", id) } return user, apperrors.NewInternal() } return user, nil } // DeleteRequest removes the given member and user from the friend requests func (r *friendRepository) DeleteRequest(memberId string, userId string) error { return r.DB.Exec(` DELETE FROM friend_requests WHERE receiver_id = @memberId AND sender_id = @userId OR receiver_id = @userId AND sender_id = @memberId `, sql.Named("memberId", memberId), sql.Named("userId", userId)).Error } // RemoveFriend removes members from the friends table. func (r *friendRepository) RemoveFriend(memberId string, userId string) error { return r.DB. Exec("DELETE FROM friends WHERE user_id = ? AND friend_id = ?", memberId, userId). Exec("DELETE FROM friends WHERE user_id = ? AND friend_id = ?", userId, memberId). Error } // Save inserts the given user in the DB func (r *friendRepository) Save(user *model.User) error { return r.DB.Save(&user).Error } ================================================ FILE: server/repository/guild_repository.go ================================================ package repository import ( "errors" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "gorm.io/gorm" "gorm.io/gorm/clause" "log" "time" ) // guildRepository is data/repository implementation // of service layer GuildRepository type guildRepository struct { DB *gorm.DB } // NewGuildRepository is a factory for initializing Guild Repositories func NewGuildRepository(db *gorm.DB) model.GuildRepository { return &guildRepository{ DB: db, } } // List returns all of the given users guilds func (r *guildRepository) List(uid string) (*[]model.GuildResponse, error) { var guilds []model.GuildResponse result := r.DB.Raw(` SELECT distinct g."id", g."name", g."owner_id", g."icon", g."created_at", g."updated_at", ((SELECT c."last_activity" FROM channels c JOIN guilds g ON g.id = c."guild_id" WHERE g.id = member."guild_id" order by c."last_activity" DESC limit 1) > member."last_seen") AS "hasNotification", (SELECT c.id AS "default_channel_id" FROM channels c JOIN guilds g ON g.id = c."guild_id" WHERE g.id = member."guild_id" ORDER BY c."created_at" LIMIT 1) FROM guilds g JOIN members as member on g."id"::text = member."guild_id" WHERE member."user_id" = ? ORDER BY g."created_at"; `, uid).Find(&guilds) return &guilds, result.Error } // GuildMembers returns all members of the given guild and // whether they are the given user IDs friend func (r *guildRepository) GuildMembers(userId string, guildId string) (*[]model.MemberResponse, error) { var members []model.MemberResponse result := r.DB.Raw(` SELECT u.id, u.username, u.image, u."is_online", u."created_at", u."updated_at", m.nickname, m.color, EXISTS( SELECT 1 FROM users LEFT JOIN friends f ON users.id = f."user_id" WHERE f."friend_id" = u.id AND f."user_id" = ? ) AS is_friend FROM users AS u JOIN members m ON u."id"::text = m."user_id" WHERE m."guild_id" = ? ORDER BY (CASE WHEN m.nickname notnull THEN m.nickname ELSE u.username END) `, userId, guildId).Find(&members) return &members, result.Error } // Create inserts the given guild in the DB func (r *guildRepository) Create(guild *model.Guild) (*model.Guild, error) { if result := r.DB.Create(&guild); result.Error != nil { log.Printf("Could not create a guild for user: %v. Reason: %v\n", guild.OwnerId, result.Error) return nil, apperrors.NewInternal() } return guild, nil } // FindUserByID returns a user containing all of their guilds func (r *guildRepository) FindUserByID(uid string) (*model.User, error) { user := &model.User{} // we need to actually check errors as it could be something other than not found if err := r.DB. Preload("Guilds"). Where("id = ?", uid). First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return user, apperrors.NewNotFound("uid", uid) } return user, apperrors.NewInternal() } return user, nil } // FindByID returns the guild for the given id containing all of their fields func (r *guildRepository) FindByID(id string) (*model.Guild, error) { guild := &model.Guild{} if err := r.DB. Preload(clause.Associations). Where("id = ?", id). First(&guild).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return guild, apperrors.NewNotFound("id", id) } return guild, apperrors.NewInternal() } return guild, nil } // Save updates the given guild func (r *guildRepository) Save(guild *model.Guild) error { if result := r.DB.Save(&guild); result.Error != nil { log.Printf("Could not update the guild with id: %v. Reason: %v\n", guild.ID, result.Error) return apperrors.NewInternal() } return nil } // RemoveMember removes the given user from the given guild func (r *guildRepository) RemoveMember(userId string, guildId string) error { if result := r.DB. Exec("DELETE FROM members WHERE user_id = ? AND guild_id = ?", userId, guildId); result.Error != nil { log.Printf("Could not remove member with id: %s from the guild with id: %v. Reason: %v\n", userId, guildId, result.Error) return apperrors.NewInternal() } return nil } // Delete removes the given guild and all its associations func (r *guildRepository) Delete(guildId string) error { if result := r.DB. Exec("DELETE FROM members WHERE guild_id = ?", guildId). Exec("DELETE FROM bans WHERE guild_id = ?", guildId). Exec("DELETE FROM guilds WHERE id = ?", guildId); result.Error != nil { log.Printf("Could not delete the guild with id: %v. Reason: %v\n", guildId, result.Error) return apperrors.NewInternal() } return nil } // UnbanMember removes the given user from the bans of the given guild func (r *guildRepository) UnbanMember(userId string, guildId string) error { if result := r.DB.Exec("DELETE FROM bans WHERE guild_id = ? AND user_id = ?", guildId, userId); result.Error != nil { log.Printf("Could not unban the user with id: %v from the guild with id: %v. Reason: %v\n", userId, guildId, result.Error) return apperrors.NewInternal() } return nil } // GetBanList returns a list of all banned users from the given guild func (r *guildRepository) GetBanList(guildId string) (*[]model.BanResponse, error) { var bans []model.BanResponse if result := r.DB.Raw(` select u.id, u.username, u.image from bans b join users u on b."user_id" = u.id where b."guild_id" = ? `, guildId).Scan(&bans); result.Error != nil { log.Printf("Could not get the ban list for the guild with id: %v. Reason: %v\n", guildId, result.Error) return &bans, apperrors.NewInternal() } return &bans, nil } // GetMemberSettings returns the given members settings in the given guild func (r *guildRepository) GetMemberSettings(userId string, guildId string) (*model.MemberSettings, error) { settings := model.MemberSettings{} err := r.DB. Table("members"). Where("user_id = ? AND guild_id = ?", userId, guildId). First(&settings) return &settings, err.Error } // UpdateMemberSettings updates the settings of the given member in the given guild func (r *guildRepository) UpdateMemberSettings(settings *model.MemberSettings, userId string, guildId string) error { err := r.DB. Table("members"). Where("user_id = ? AND guild_id = ?", userId, guildId). Updates(map[string]any{ "color": settings.Color, "nickname": settings.Nickname, "updated_at": time.Now(), }). Error return err } // FindUsersByIds returns the found users for the given user IDs and guild ID func (r *guildRepository) FindUsersByIds(ids []string, guildId string) (*[]model.User, error) { var users []model.User result := r.DB.Raw(` SELECT u.* FROM users AS u JOIN members m ON u."id"::text = m."user_id" WHERE m."guild_id" = ? AND m."user_id" IN ? `, guildId, ids).Find(&users) return &users, result.Error } // GetMember returns user for the given userId and guildId func (r *guildRepository) GetMember(userId, guildId string) (*model.User, error) { var user model.User result := r.DB.Raw(` SELECT u.* FROM users AS u JOIN members m ON u."id"::text = m."user_id" WHERE m."guild_id" = ? AND m."user_id" = ? `, guildId, userId).Find(&user) return &user, result.Error } // UpdateMemberLastSeen sets the LastSeen field of the given user to the current date func (r *guildRepository) UpdateMemberLastSeen(userId, guildId string) error { err := r.DB. Table("members"). Where("user_id = ? AND guild_id = ?", userId, guildId). Updates(map[string]any{ "last_seen": time.Now(), }). Error return err } // GetMemberIds returns the ids of all members of the given guild func (r *guildRepository) GetMemberIds(guildId string) (*[]string, error) { var users []string result := r.DB.Raw(` SELECT u.id FROM users AS u JOIN members m ON u."id"::text = m."user_id" WHERE m."guild_id" = ? `, guildId).Find(&users) return &users, result.Error } func (r *guildRepository) RemoveVCMember(userId, guildId string) error { if result := r.DB.Exec("DELETE FROM vc_members WHERE guild_id = ? AND user_id = ?", guildId, userId); result.Error != nil { log.Printf("Could not add the user with id: %v to the vc of the guild with id: %v. Reason: %v\n", userId, guildId, result.Error) return apperrors.NewInternal() } return nil } func (r *guildRepository) VCMembers(guildId string) (*[]model.VCMemberResponse, error) { var members []model.VCMemberResponse result := r.DB.Raw(` SELECT u.id, u.username, u.image, vm.is_muted, vm.is_deafened, m.nickname FROM users AS u JOIN vc_members vm ON u."id"::text = vm."user_id" JOIN members m on vm.user_id = m.user_id AND vm.guild_id = m.guild_id WHERE vm."guild_id" = ? ORDER BY (CASE WHEN m.nickname notnull THEN m.nickname ELSE u.username END) `, guildId).Find(&members) return &members, result.Error } func (r *guildRepository) UpdateVCMember(isMuted, isDeafened bool, userId, guildId string) error { err := r.DB. Table("vc_members"). Where("user_id = ? AND guild_id = ?", userId, guildId). Updates(map[string]any{ "is_muted": isMuted, "is_deafened": isDeafened, }). Error return err } func (r *guildRepository) GetVCMember(userId, guildId string) (*model.VCMember, error) { var user model.VCMember result := r.DB.Raw(` SELECT * from vc_members vm WHERE vm."guild_id" = ? AND vm."user_id" = ? `, guildId, userId).Find(&user) return &user, result.Error } ================================================ FILE: server/repository/mail_repository.go ================================================ package repository import ( "fmt" "github.com/sentrionic/valkyrie/model" "net/smtp" ) // mailRepository contains the gmail username and password // as well as the frontend origin. type mailRepository struct { username string password string origin string } // NewMailRepository is a factory for initializing Mail Repositories func NewMailRepository(username string, password string, origin string) model.MailRepository { return &mailRepository{ username: username, password: password, origin: origin, } } // SendResetMail sends a password reset email with the given reset token func (m *mailRepository) SendResetMail(email string, token string) error { msg := "From: " + m.username + "\n" + "To: " + email + "\n" + "Subject: Reset Email\n\n" + fmt.Sprintf("Reset Password", m.origin, token) err := smtp.SendMail("smtp.gmail.com:587", smtp.PlainAuth("", m.username, m.password, "smtp.gmail.com"), m.username, []string{email}, []byte(msg)) return err } ================================================ FILE: server/repository/message_repository.go ================================================ package repository import ( "database/sql" "errors" "fmt" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "gorm.io/gorm" "log" "time" ) // messageRepository is data/repository implementation // of service layer MessageRepository type messageRepository struct { DB *gorm.DB } // NewMessageRepository is a factory for initializing Message Repositories func NewMessageRepository(db *gorm.DB) model.MessageRepository { return &messageRepository{ DB: db, } } // messageQuery represents the fetched fields for GetMessages type messageQuery struct { Id string Text *string CreatedAt time.Time UpdatedAt time.Time FileType *string Url *string Filename *string AttachmentId *string UserId string UserCreatedAt time.Time UserUpdatedAt time.Time Username string Image string IsOnline bool Nickname *string Color *string IsFriend bool } // GetMessages returns the 35 most recent messages for the given channel. // If a cursor is specified it returns the 35 messages after the cursor. func (r *messageRepository) GetMessages(userId string, channel *model.Channel, cursor string) (*[]model.MessageResponse, error) { var result []messageQuery memberSelect := "" memberJoin := "" memberWhere := "" // If the channel is not a DM channel, also fetch the message author's settings if !channel.IsDM { memberSelect = "member.nickname, member.color," memberJoin = "LEFT JOIN members member on messages.user_id = member.user_id" memberWhere = fmt.Sprintf("AND member.guild_id = %s::text", *channel.GuildID) } crs := "" if cursor != "" { // Remove the timezone from the string since it's stored differently in the DB date := cursor[:len(cursor)-6] crs = fmt.Sprintf("AND messages.created_at < '%s'", date) } err := r.DB. Raw(fmt.Sprintf(` SELECT messages.id, messages.text, messages.created_at, messages.updated_at, a.file_type, a.url, a.filename, a.id as "attachment_id", users.id as "user_id", users.created_at as "user_created_at", users.updated_at as "user_updated_at", users.username, users.image, users.is_online, %s EXISTS( SELECT 1 FROM users LEFT JOIN friends f ON users.id = f.user_id WHERE f.friend_id = messages.user_id AND f.user_id = @userId) as is_friend FROM messages LEFT JOIN "users" ON users.id = messages.user_id LEFT JOIN attachments a ON a.message_id = messages.id %s WHERE messages.channel_id = @channelId %s %s ORDER BY messages.created_at DESC LIMIT 35 `, memberSelect, memberJoin, memberWhere, crs), sql.Named("userId", userId), sql.Named("channelId", channel.ID)). Scan(&result).Error var messages []model.MessageResponse // Turn messageQuery results into MessageResponse for _, m := range result { var attachment *model.Attachment = nil if m.AttachmentId != nil { attachment = &model.Attachment{ Url: *m.Url, FileType: *m.FileType, Filename: *m.Filename, } } message := model.MessageResponse{ Id: m.Id, Text: m.Text, CreatedAt: m.CreatedAt, UpdatedAt: m.UpdatedAt, Attachment: attachment, User: model.MemberResponse{ Id: m.UserId, Username: m.Username, Image: m.Image, IsOnline: m.IsOnline, CreatedAt: m.UserCreatedAt, UpdatedAt: m.UserUpdatedAt, Nickname: m.Nickname, Color: m.Color, IsFriend: m.IsFriend, }, } messages = append(messages, message) } return &messages, err } // CreateMessage inserts the message in the DB func (r *messageRepository) CreateMessage(message *model.Message) (*model.Message, error) { if result := r.DB.Create(&message); result.Error != nil { log.Printf("Could not create a message for user: %v. Reason: %v\n", message.UserId, result.Error) return nil, apperrors.NewInternal() } return message, nil } // UpdateMessage updates the message in the DB func (r *messageRepository) UpdateMessage(message *model.Message) error { if result := r.DB.Save(&message); result.Error != nil { log.Printf("Could not update message with id: %v. Reason: %v\n", message.ID, result.Error) return apperrors.NewInternal() } return nil } // DeleteMessage removes the message from the DB func (r *messageRepository) DeleteMessage(message *model.Message) error { if result := r.DB.Delete(message); result.Error != nil { log.Printf("Could not delete message with id: %v. Reason: %v\n", message.ID, result.Error) return apperrors.NewInternal() } return nil } // GetById fetches the message for the given id func (r *messageRepository) GetById(messageId string) (*model.Message, error) { message := &model.Message{} if result := r.DB.Where("id = ?", messageId).First(message); result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return message, apperrors.NewNotFound("message", messageId) } return message, apperrors.NewInternal() } return message, nil } ================================================ FILE: server/repository/redis_repository.go ================================================ package repository import ( "context" "encoding/json" "fmt" gonanoid "github.com/matoous/go-nanoid/v2" "github.com/redis/go-redis/v9" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "log" "time" ) type redisRepository struct { rds *redis.Client } // NewRedisRepository is a factory for initializing Redis Repositories func NewRedisRepository(rds *redis.Client) model.RedisRepository { return &redisRepository{ rds: rds, } } // Redis Prefixes const ( InviteLinkPrefix = "inviteLink" ForgotPasswordPrefix = "forgot-password" ) // SetResetToken inserts a password reset token in the DB and returns the generated token func (r *redisRepository) SetResetToken(ctx context.Context, id string) (string, error) { uid, err := gonanoid.New() if err != nil { log.Printf("Failed to generate id: %v\n", err.Error()) return "", apperrors.NewInternal() } if err = r.rds.Set(ctx, fmt.Sprintf("%s:%s", ForgotPasswordPrefix, uid), id, 24*time.Hour).Err(); err != nil { log.Printf("Failed to set link in redis: %v\n", err.Error()) return "", apperrors.NewInternal() } return uid, nil } // GetIdFromToken returns the user ID from the DB for the given token func (r *redisRepository) GetIdFromToken(ctx context.Context, token string) (string, error) { key := fmt.Sprintf("%s:%s", ForgotPasswordPrefix, token) val, err := r.rds.Get(ctx, key).Result() if err == redis.Nil { return "", apperrors.NewBadRequest(apperrors.InvalidResetToken) } if err != nil { log.Printf("Failed to get value from redis: %v\n", err) return "", apperrors.NewInternal() } r.rds.Del(ctx, key) return val, nil } // SaveInvite inserts an invite for the given guild in the DB. // If isPermanent is true, the invite won't expire func (r *redisRepository) SaveInvite(ctx context.Context, guildId string, id string, isPermanent bool) error { invite := model.Invite{GuildId: guildId, IsPermanent: isPermanent} value, err := json.Marshal(invite) if err != nil { log.Printf("Error marshalling: %v\n", err.Error()) return apperrors.NewInternal() } expiration := 24 * time.Hour if isPermanent { expiration = 0 } if result := r.rds.Set(ctx, fmt.Sprintf("%s:%s", InviteLinkPrefix, id), value, expiration); result.Err() != nil { log.Printf("Failed to set invite link in redis: %v\n", err.Error()) return apperrors.NewInternal() } return nil } // GetInvite returns the stored guild Id for the given token. func (r *redisRepository) GetInvite(ctx context.Context, token string) (string, error) { key := fmt.Sprintf("%s:%s", InviteLinkPrefix, token) val, err := r.rds.Get(ctx, key).Result() if err != nil { log.Printf("Failed to get invite link from redis: %v\n", err.Error()) return "", apperrors.NewInternal() } var invite model.Invite err = json.Unmarshal([]byte(val), &invite) if err != nil { log.Printf("Error unmarshalling: %v\n", err.Error()) return "", apperrors.NewInternal() } if !invite.IsPermanent { r.rds.Del(ctx, key) } return invite.GuildId, nil } // InvalidateInvites deletes all permanent invites in the DB for the given guild func (r *redisRepository) InvalidateInvites(ctx context.Context, guild *model.Guild) { for _, v := range guild.InviteLinks { key := fmt.Sprintf("%s:%s", InviteLinkPrefix, v) r.rds.Del(ctx, key) } } ================================================ FILE: server/repository/user_repository.go ================================================ package repository import ( "database/sql" "errors" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "gorm.io/gorm" "log" "regexp" ) // userRepository is data/repository implementation // of service layer UserRepository type userRepository struct { DB *gorm.DB } // NewUserRepository is a factory for initializing User Repositories func NewUserRepository(db *gorm.DB) model.UserRepository { return &userRepository{ DB: db, } } // FindByID returns a user for the given ID func (r *userRepository) FindByID(id string) (*model.User, error) { user := &model.User{} // we need to actually check errors as it could be something other than not found if err := r.DB.Where("id = ?", id).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return user, apperrors.NewNotFound("uid", id) } return user, apperrors.NewInternal() } return user, nil } // Create inserts the user in the DB func (r *userRepository) Create(user *model.User) (*model.User, error) { if result := r.DB.Create(&user); result.Error != nil { // check unique constraint if isDuplicateKeyError(result.Error) { return nil, apperrors.NewBadRequest(apperrors.DuplicateEmail) } log.Printf("Could not create a user with email: %v. Reason: %v\n", user.Email, result.Error) return nil, apperrors.NewInternal() } return user, nil } // FindByEmail retrieves user row by email address func (r *userRepository) FindByEmail(email string) (*model.User, error) { user := &model.User{} // we need to actually check errors as it could be something other than not found if err := r.DB.Where("email = ?", email).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return user, apperrors.NewNotFound("email", email) } return user, apperrors.NewInternal() } return user, nil } // Update updates the user in the DB func (r *userRepository) Update(user *model.User) error { return r.DB.Save(&user).Error } // GetFriendAndGuildIds returns the id of the users friends and the guilds they are part of func (r *userRepository) GetFriendAndGuildIds(userId string) (*[]string, error) { var ids []string result := r.DB.Raw(` SELECT g.id FROM guilds g JOIN members m on m.guild_id = g."id" where m.user_id = @userId UNION SELECT "User__friends"."id" FROM "users" "User" LEFT JOIN "friends" "User_User__friends" ON "User_User__friends"."user_id"="User"."id" LEFT JOIN "users" "User__friends" ON "User__friends"."id"="User_User__friends"."friend_id" WHERE ( "User"."id" = @userId ) `, sql.Named("userId", userId)).Find(&ids) return &ids, result.Error } // GetRequestCount returns the amount of incoming friend requests the current user currently has func (r *userRepository) GetRequestCount(userId string) (*int64, error) { var count int64 err := r.DB. Table("users"). Joins("JOIN friend_requests fr ON users.id = fr.sender_id"). Where("fr.receiver_id = ?", userId). Count(&count). Error return &count, err } // isDuplicateKeyError checks if the provided error is a PostgreSQL duplicate key error func isDuplicateKeyError(err error) bool { duplicate := regexp.MustCompile(`\(SQLSTATE 23505\)$`) return duplicate.MatchString(err.Error()) } ================================================ FILE: server/service/channel_service.go ================================================ package service import ( "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" ) // channelService acts as a struct for injecting an implementation of ChannelRepository // for use in service methods type channelService struct { ChannelRepository model.ChannelRepository GuildRepository model.GuildRepository } // CSConfig will hold repositories that will eventually be injected into // this service layer type CSConfig struct { ChannelRepository model.ChannelRepository GuildRepository model.GuildRepository } // NewChannelService is a factory function for // initializing a ChannelService with its repository layer dependencies func NewChannelService(c *CSConfig) model.ChannelService { return &channelService{ ChannelRepository: c.ChannelRepository, GuildRepository: c.GuildRepository, } } func (c *channelService) CreateChannel(channel *model.Channel) (*model.Channel, error) { channel.ID = GenerateId() return c.ChannelRepository.Create(channel) } func (c *channelService) GetChannels(userId string, guildId string) (*[]model.ChannelResponse, error) { return c.ChannelRepository.Get(userId, guildId) } func (c *channelService) Get(channelId string) (*model.Channel, error) { return c.ChannelRepository.GetById(channelId) } func (c *channelService) GetPrivateChannelMembers(channelId string) (*[]string, error) { return c.ChannelRepository.GetPrivateChannelMembers(channelId) } func (c *channelService) GetDirectMessages(userId string) (*[]model.DirectMessage, error) { return c.ChannelRepository.GetDirectMessages(userId) } func (c *channelService) GetDirectMessageChannel(userId string, memberId string) (*string, error) { return c.ChannelRepository.GetDirectMessageChannel(userId, memberId) } func (c *channelService) AddDMChannelMembers(memberIds []string, channelId string, userId string) error { var members []model.DMMember for _, mId := range memberIds { member := model.DMMember{ ID: GenerateId(), UserID: mId, ChannelId: channelId, IsOpen: userId == mId, } members = append(members, member) } return c.ChannelRepository.AddDMChannelMembers(members) } func (c *channelService) SetDirectMessageStatus(dmId string, userId string, isOpen bool) error { return c.ChannelRepository.SetDirectMessageStatus(dmId, userId, isOpen) } func (c *channelService) DeleteChannel(channel *model.Channel) error { return c.ChannelRepository.DeleteChannel(channel) } func (c *channelService) UpdateChannel(channel *model.Channel) error { return c.ChannelRepository.UpdateChannel(channel) } func (c *channelService) CleanPCMembers(channelId string) error { return c.ChannelRepository.CleanPCMembers(channelId) } func (c *channelService) AddPrivateChannelMembers(memberIds []string, channelId string) error { return c.ChannelRepository.AddPrivateChannelMembers(memberIds, channelId) } func (c *channelService) RemovePrivateChannelMembers(memberIds []string, channelId string) error { return c.ChannelRepository.RemovePrivateChannelMembers(memberIds, channelId) } func (c *channelService) OpenDMForAll(dmId string) error { return c.ChannelRepository.OpenDMForAll(dmId) } func (c *channelService) GetDMByUserAndChannel(userId string, channelId string) (string, error) { return c.ChannelRepository.FindDMByUserAndChannelId(channelId, userId) } // IsChannelMember checks if the user has access to the given channel. // Returns an error if they do not, otherwise nil func (c *channelService) IsChannelMember(channel *model.Channel, userId string) error { // Check if user has access to the channel if it's private if !channel.IsPublic { // Channel is DM -> Check if one of the members if channel.IsDM { id, err := c.ChannelRepository.FindDMByUserAndChannelId(channel.ID, userId) if err != nil || id == "" { return apperrors.NewAuthorization(apperrors.Unauthorized) } return nil } // Channel is private for _, member := range channel.PCMembers { if member.ID == userId { return nil } } return apperrors.NewAuthorization(apperrors.Unauthorized) } // Check if user has access to the channel member, err := c.GuildRepository.GetMember(userId, *channel.GuildID) if err != nil || member.ID == "" { return apperrors.NewAuthorization(apperrors.Unauthorized) } return nil } ================================================ FILE: server/service/channel_service_test.go ================================================ package service import ( "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "testing" ) func TestChannelService_CreateChannel(t *testing.T) { t.Run("Success", func(t *testing.T) { uid := GenerateId() mockChannel := fixture.GetMockChannel("") params := &model.Channel{ GuildID: mockChannel.GuildID, Name: mockChannel.Name, IsPublic: true, } mockChannelRepository := new(mocks.ChannelRepository) cs := NewChannelService(&CSConfig{ ChannelRepository: mockChannelRepository, }) mockChannelRepository. On("Create", params). Run(func(args mock.Arguments) { mockChannel.ID = uid }).Return(mockChannel, nil) channel, err := cs.CreateChannel(params) assert.NoError(t, err) assert.Equal(t, uid, mockChannel.ID) assert.Equal(t, channel, mockChannel) mockChannelRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockChannel := fixture.GetMockChannel("") params := &model.Channel{ GuildID: mockChannel.GuildID, Name: mockChannel.Name, IsPublic: true, } mockChannelRepository := new(mocks.ChannelRepository) cs := NewChannelService(&CSConfig{ ChannelRepository: mockChannelRepository, }) mockErr := apperrors.NewInternal() mockChannelRepository. On("Create", params). Return(nil, mockErr) channel, err := cs.CreateChannel(params) // assert error is error we response with in mock assert.EqualError(t, err, mockErr.Error()) assert.Nil(t, channel) mockChannelRepository.AssertExpectations(t) }) } func TestChannelService_AddDMChannelMembers(t *testing.T) { userId := fixture.RandID() ids := []string{userId, fixture.RandID()} channelId := fixture.RandID() t.Run("Success", func(t *testing.T) { mockChannelRepository := new(mocks.ChannelRepository) cs := NewChannelService(&CSConfig{ ChannelRepository: mockChannelRepository, }) mockChannelRepository. On("AddDMChannelMembers", mock.AnythingOfType("[]model.DMMember")). // Has to be added so the different IDs get accepted Return(nil) err := cs.AddDMChannelMembers(ids, channelId, userId) assert.NoError(t, err) mockChannelRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockChannelRepository := new(mocks.ChannelRepository) cs := NewChannelService(&CSConfig{ ChannelRepository: mockChannelRepository, }) mockError := apperrors.NewInternal() mockChannelRepository. On("AddDMChannelMembers", mock.AnythingOfType("[]model.DMMember")). // Has to be added so the different IDs get accepted Return(mockError) err := cs.AddDMChannelMembers(ids, channelId, userId) assert.Error(t, err) mockChannelRepository.AssertExpectations(t) }) } func TestChannelService_IsChannelMember(t *testing.T) { mockUser := fixture.GetMockUser() t.Run("User is member of the DM", func(t *testing.T) { mockChannel := fixture.GetMockDMChannel() mockChannelRepository := new(mocks.ChannelRepository) cs := NewChannelService(&CSConfig{ ChannelRepository: mockChannelRepository, }) mockChannelRepository.On("FindDMByUserAndChannelId", mockChannel.ID, mockUser.ID).Return(fixture.RandID(), nil) err := cs.IsChannelMember(mockChannel, mockUser.ID) assert.NoError(t, err) }) t.Run("User is not member of the DM", func(t *testing.T) { mockChannel := fixture.GetMockDMChannel() mockChannelRepository := new(mocks.ChannelRepository) cs := NewChannelService(&CSConfig{ ChannelRepository: mockChannelRepository, }) mockError := apperrors.NewAuthorization(apperrors.Unauthorized) mockChannelRepository.On("FindDMByUserAndChannelId", mockChannel.ID, mockUser.ID).Return("", mockError) err := cs.IsChannelMember(mockChannel, mockUser.ID) assert.Error(t, err) assert.Equal(t, err, mockError) }) t.Run("User is member of the private channel", func(t *testing.T) { mockChannel := fixture.GetMockChannel("") mockChannel.IsPublic = false mockChannel.PCMembers = append(mockChannel.PCMembers, *mockUser) mockChannelRepository := new(mocks.ChannelRepository) cs := NewChannelService(&CSConfig{ ChannelRepository: mockChannelRepository, }) err := cs.IsChannelMember(mockChannel, mockUser.ID) assert.NoError(t, err) }) t.Run("User is not member of the private channel", func(t *testing.T) { mockChannel := fixture.GetMockChannel("") mockChannel.IsPublic = false mockChannelRepository := new(mocks.ChannelRepository) cs := NewChannelService(&CSConfig{ ChannelRepository: mockChannelRepository, }) err := cs.IsChannelMember(mockChannel, mockUser.ID) assert.Error(t, err) assert.Equal(t, err, apperrors.NewAuthorization(apperrors.Unauthorized)) }) t.Run("User is a guild member", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockGuild.Members = append(mockGuild.Members, *mockUser) mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildRepository := new(mocks.GuildRepository) cs := NewChannelService(&CSConfig{ GuildRepository: mockGuildRepository, }) mockGuildRepository.On("GetMember", mockUser.ID, *mockChannel.GuildID).Return(mockUser, nil) err := cs.IsChannelMember(mockChannel, mockUser.ID) assert.NoError(t, err) }) t.Run("User is not a member of the guild", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") mockChannel := fixture.GetMockChannel(mockGuild.ID) mockGuildRepository := new(mocks.GuildRepository) cs := NewChannelService(&CSConfig{ GuildRepository: mockGuildRepository, }) mockError := apperrors.NewAuthorization(apperrors.Unauthorized) mockGuildRepository.On("GetMember", mockUser.ID, *mockChannel.GuildID).Return(nil, mockError) err := cs.IsChannelMember(mockChannel, mockUser.ID) assert.Error(t, err) assert.Equal(t, err, mockError) }) } ================================================ FILE: server/service/friend_service.go ================================================ package service import ( "github.com/sentrionic/valkyrie/model" ) // friendService acts as a struct for injecting an implementation of UserRepository // and FriendRepository for use in service methods type friendService struct { UserRepository model.UserRepository FriendRepository model.FriendRepository } // FSConfig will hold repositories that will eventually be injected into // this service layer type FSConfig struct { UserRepository model.UserRepository FriendRepository model.FriendRepository } // NewFriendService is a factory function for // initializing a FriendService with its repository layer dependencies func NewFriendService(c *FSConfig) model.FriendService { return &friendService{ UserRepository: c.UserRepository, FriendRepository: c.FriendRepository, } } func (f *friendService) GetFriends(id string) (*[]model.Friend, error) { return f.FriendRepository.FriendsList(id) } func (f *friendService) GetRequests(id string) (*[]model.FriendRequest, error) { return f.FriendRepository.RequestList(id) } func (f *friendService) GetMemberById(id string) (*model.User, error) { return f.FriendRepository.FindByID(id) } func (f *friendService) DeleteRequest(memberId string, userId string) error { return f.FriendRepository.DeleteRequest(memberId, userId) } func (f *friendService) RemoveFriend(memberId string, userId string) error { return f.FriendRepository.RemoveFriend(memberId, userId) } func (f *friendService) SaveRequests(user *model.User) error { return f.FriendRepository.Save(user) } ================================================ FILE: server/service/guild_service.go ================================================ package service import ( "context" gonanoid "github.com/matoous/go-nanoid" "github.com/sentrionic/valkyrie/model" ) // GuildService acts as a struct for injecting an implementation of GuildRepository // for use in service methods type guildService struct { UserRepository model.UserRepository FileRepository model.FileRepository RedisRepository model.RedisRepository GuildRepository model.GuildRepository ChannelRepository model.ChannelRepository } // GSConfig will hold repositories that will eventually be injected into // this service layer type GSConfig struct { UserRepository model.UserRepository FileRepository model.FileRepository RedisRepository model.RedisRepository GuildRepository model.GuildRepository ChannelRepository model.ChannelRepository } // NewGuildService is a factory function for // initializing a GuildService with its repository layer dependencies func NewGuildService(c *GSConfig) model.GuildService { return &guildService{ UserRepository: c.UserRepository, FileRepository: c.FileRepository, RedisRepository: c.RedisRepository, GuildRepository: c.GuildRepository, ChannelRepository: c.ChannelRepository, } } func (g *guildService) GetUserGuilds(uid string) (*[]model.GuildResponse, error) { return g.GuildRepository.List(uid) } func (g *guildService) GetGuildMembers(userId string, guildId string) (*[]model.MemberResponse, error) { return g.GuildRepository.GuildMembers(userId, guildId) } func (g *guildService) CreateGuild(guild *model.Guild) (*model.Guild, error) { guild.ID = GenerateId() return g.GuildRepository.Create(guild) } func (g *guildService) GetUser(uid string) (*model.User, error) { return g.GuildRepository.FindUserByID(uid) } func (g *guildService) GetGuild(id string) (*model.Guild, error) { return g.GuildRepository.FindByID(id) } func (g *guildService) GenerateInviteLink(ctx context.Context, guildId string, isPermanent bool) (string, error) { id, err := gonanoid.Nanoid(8) if err != nil { return "", err } if err = g.RedisRepository.SaveInvite(ctx, guildId, id, isPermanent); err != nil { return "", err } return id, nil } func (g *guildService) UpdateGuild(guild *model.Guild) error { return g.GuildRepository.Save(guild) } func (g *guildService) GetGuildIdFromInvite(ctx context.Context, token string) (string, error) { return g.RedisRepository.GetInvite(ctx, token) } func (g *guildService) GetDefaultChannel(guildId string) (*model.Channel, error) { return g.ChannelRepository.GetGuildDefault(guildId) } func (g *guildService) InvalidateInvites(ctx context.Context, guild *model.Guild) { g.RedisRepository.InvalidateInvites(ctx, guild) } func (g *guildService) RemoveMember(userId string, guildId string) error { return g.GuildRepository.RemoveMember(userId, guildId) } func (g *guildService) DeleteGuild(guildId string) error { return g.GuildRepository.Delete(guildId) } func (g *guildService) UnbanMember(userId string, guildId string) error { return g.GuildRepository.UnbanMember(userId, guildId) } func (g *guildService) GetBanList(guildId string) (*[]model.BanResponse, error) { return g.GuildRepository.GetBanList(guildId) } func (g *guildService) GetMemberSettings(userId string, guildId string) (*model.MemberSettings, error) { return g.GuildRepository.GetMemberSettings(userId, guildId) } func (g *guildService) UpdateMemberSettings(settings *model.MemberSettings, userId string, guildId string) error { return g.GuildRepository.UpdateMemberSettings(settings, userId, guildId) } func (g *guildService) FindUsersByIds(ids []string, guildId string) (*[]model.User, error) { return g.GuildRepository.FindUsersByIds(ids, guildId) } func (g *guildService) UpdateMemberLastSeen(userId, guildId string) error { return g.GuildRepository.UpdateMemberLastSeen(userId, guildId) } func (g *guildService) RemoveVCMember(userId, guildId string) error { return g.GuildRepository.RemoveVCMember(userId, guildId) } func (g *guildService) GetVCMembers(guildId string) (*[]model.VCMemberResponse, error) { return g.GuildRepository.VCMembers(guildId) } func (g *guildService) UpdateVCMember(isMuted, isDeafened bool, userId, guildId string) error { return g.GuildRepository.UpdateVCMember(isMuted, isDeafened, userId, guildId) } func (g *guildService) GetVCMember(userId, guildId string) (*model.VCMember, error) { return g.GuildRepository.GetVCMember(userId, guildId) } ================================================ FILE: server/service/guild_service_test.go ================================================ package service import ( "context" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "testing" ) func TestGuildService_CreateGuild(t *testing.T) { t.Run("Success", func(t *testing.T) { uid := GenerateId() mockGuild := fixture.GetMockGuild("") params := &model.Guild{ Name: mockGuild.Name, } mockGuildRepository := new(mocks.GuildRepository) gs := NewGuildService(&GSConfig{ GuildRepository: mockGuildRepository, }) mockGuildRepository. On("Create", params). Run(func(args mock.Arguments) { mockGuild.ID = uid }).Return(mockGuild, nil) guild, err := gs.CreateGuild(params) assert.NoError(t, err) assert.Equal(t, uid, mockGuild.ID) assert.Equal(t, guild, mockGuild) mockGuildRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockGuild := fixture.GetMockGuild("") params := &model.Guild{ Name: mockGuild.Name, } mockGuildRepository := new(mocks.GuildRepository) gs := NewGuildService(&GSConfig{ GuildRepository: mockGuildRepository, }) mockErr := apperrors.NewInternal() mockGuildRepository. On("Create", params). Return(nil, mockErr) guild, err := gs.CreateGuild(params) // assert error is error we response with in mock assert.EqualError(t, err, mockErr.Error()) assert.Nil(t, guild) mockGuildRepository.AssertExpectations(t) }) } func TestGuildService_GenerateInviteLink(t *testing.T) { guildId := fixture.RandID() ctx := context.TODO() t.Run("Success", func(t *testing.T) { mockRedisRepository := new(mocks.RedisRepository) gs := NewGuildService(&GSConfig{ RedisRepository: mockRedisRepository, }) args := mock.Arguments{ ctx, guildId, mock.AnythingOfType("string"), false, } mockRedisRepository. On("SaveInvite", args...). Run(func(args mock.Arguments) {}). Return(nil) link, err := gs.GenerateInviteLink(ctx, guildId, false) assert.NoError(t, err) assert.NotEqual(t, link, "") mockRedisRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockRedisRepository := new(mocks.RedisRepository) gs := NewGuildService(&GSConfig{ RedisRepository: mockRedisRepository, }) args := mock.Arguments{ ctx, guildId, mock.AnythingOfType("string"), false, } mockError := apperrors.NewInternal() mockRedisRepository. On("SaveInvite", args...). Run(func(args mock.Arguments) {}). Return(mockError) link, err := gs.GenerateInviteLink(ctx, guildId, false) assert.Error(t, err) assert.Equal(t, link, "") assert.Equal(t, err, mockError) mockRedisRepository.AssertExpectations(t) }) } ================================================ FILE: server/service/id_generator.go ================================================ package service import ( "github.com/bwmarrin/snowflake" "log" ) var node *snowflake.Node func init() { const nodeID int64 = 1 var err error node, err = snowflake.NewNode(nodeID) if err != nil { log.Fatalf("failed to init snowflake node: %v", err.Error()) } } // GenerateId generates a snowflake id func GenerateId() string { // Generate a snowflake ID. id := node.Generate() return id.String() } ================================================ FILE: server/service/message_service.go ================================================ package service import ( "fmt" gonanoid "github.com/matoous/go-nanoid" "github.com/sentrionic/valkyrie/model" "log" "mime/multipart" "path" "path/filepath" "regexp" "strings" ) // messageService acts as a struct for injecting an implementation of MessageRepository // for use in service methods type messageService struct { MessageRepository model.MessageRepository FileRepository model.FileRepository } // MSConfig will hold repositories that will eventually be injected into // this service layer type MSConfig struct { MessageRepository model.MessageRepository FileRepository model.FileRepository } // NewMessageService is a factory function for // initializing a UserService with its repository layer dependencies func NewMessageService(c *MSConfig) model.MessageService { return &messageService{ MessageRepository: c.MessageRepository, FileRepository: c.FileRepository, } } func (m *messageService) GetMessages(userId string, channel *model.Channel, cursor string) (*[]model.MessageResponse, error) { return m.MessageRepository.GetMessages(userId, channel, cursor) } func (m *messageService) CreateMessage(params *model.Message) (*model.Message, error) { params.ID = GenerateId() return m.MessageRepository.CreateMessage(params) } func (m *messageService) UpdateMessage(message *model.Message) error { return m.MessageRepository.UpdateMessage(message) } func (m *messageService) DeleteMessage(message *model.Message) error { if message.Attachment != nil { if err := m.FileRepository.DeleteImage(message.Attachment.Filename); err != nil { log.Printf("Error deleting file from S3: %s", err) } } return m.MessageRepository.DeleteMessage(message) } func (m *messageService) UploadFile(header *multipart.FileHeader, channelId string) (*model.Attachment, error) { filename := formatName(header.Filename) mimetype := header.Header.Get("Content-Type") attachment := model.Attachment{ FileType: mimetype, Filename: filename, } attachment.ID = GenerateId() directory := fmt.Sprintf("channels/%s", channelId) url, err := m.FileRepository.UploadFile(header, directory, filename, mimetype) if err != nil { return nil, err } attachment.Url = url return &attachment, nil } func (m *messageService) Get(messageId string) (*model.Message, error) { return m.MessageRepository.GetById(messageId) } var re = regexp.MustCompile(`/[^a-z0-9]/g`) func formatName(filename string) string { ext := path.Ext(filename) id, _ := gonanoid.Nanoid(5) filename = strings.TrimSuffix(filename, filepath.Ext(filename)) filename = strings.ToLower(filename) filename = re.ReplaceAllString(filename, "-") return fmt.Sprintf("%s-%s%s", id, filename, ext) } ================================================ FILE: server/service/message_service_test.go ================================================ package service import ( "fmt" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "testing" ) func TestGuildService_CreateMessage(t *testing.T) { t.Run("Success", func(t *testing.T) { uid := GenerateId() mockMessage := fixture.GetMockMessage("", "") params := &model.Message{ UserId: mockMessage.UserId, ChannelId: mockMessage.ChannelId, Text: mockMessage.Text, } mockMessageRepository := new(mocks.MessageRepository) ms := NewMessageService(&MSConfig{ MessageRepository: mockMessageRepository, }) mockMessageRepository. On("CreateMessage", params). Run(func(args mock.Arguments) { mockMessage.ID = uid }).Return(mockMessage, nil) message, err := ms.CreateMessage(params) assert.NoError(t, err) assert.Equal(t, uid, mockMessage.ID) assert.Equal(t, message, mockMessage) mockMessageRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockMessage := fixture.GetMockMessage("", "") params := &model.Message{ UserId: mockMessage.UserId, ChannelId: mockMessage.ChannelId, Text: mockMessage.Text, } mockMessageRepository := new(mocks.MessageRepository) ms := NewMessageService(&MSConfig{ MessageRepository: mockMessageRepository, }) mockErr := apperrors.NewInternal() mockMessageRepository. On("CreateMessage", params). Return(nil, mockErr) message, err := ms.CreateMessage(params) assert.EqualError(t, err, mockErr.Error()) assert.Nil(t, message) }) } func TestGuildService_DeleteMessage(t *testing.T) { t.Run("Success", func(t *testing.T) { mockMessage := fixture.GetMockMessage("", "") mockMessageRepository := new(mocks.MessageRepository) ms := NewMessageService(&MSConfig{ MessageRepository: mockMessageRepository, }) mockMessageRepository. On("DeleteMessage", mockMessage). Return(nil) err := ms.DeleteMessage(mockMessage) assert.NoError(t, err) mockMessageRepository.AssertExpectations(t) }) t.Run("Success with attachment", func(t *testing.T) { mockMessage := fixture.GetMockMessage("", "") mockMessage.Attachment = &model.Attachment{ Filename: fixture.RandStr(12), } mockMessageRepository := new(mocks.MessageRepository) mockFileRepository := new(mocks.FileRepository) ms := NewMessageService(&MSConfig{ MessageRepository: mockMessageRepository, FileRepository: mockFileRepository, }) mockFileRepository.On("DeleteImage", mockMessage.Attachment.Filename).Return(nil) mockMessageRepository. On("DeleteMessage", mockMessage). Return(nil) err := ms.DeleteMessage(mockMessage) assert.NoError(t, err) mockMessageRepository.AssertExpectations(t) mockFileRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockMessage := fixture.GetMockMessage("", "") mockMessageRepository := new(mocks.MessageRepository) ms := NewMessageService(&MSConfig{ MessageRepository: mockMessageRepository, }) mockError := apperrors.NewInternal() mockMessageRepository. On("DeleteMessage", mockMessage). Return(mockError) err := ms.DeleteMessage(mockMessage) assert.EqualError(t, err, mockError.Error()) mockMessageRepository.AssertExpectations(t) }) } func TestMessageService_UploadFile(t *testing.T) { t.Run("Success", func(t *testing.T) { imageURL := "https://imageurl.com/jdfkj34kljl" channelId := fixture.RandID() id := GenerateId() multipartImageFixture := fixture.NewMultipartImage("image.png", "image/png") defer multipartImageFixture.Close() imageFileHeader := multipartImageFixture.GetFormFile() directory := fmt.Sprintf("channels/%s", channelId) attachment := model.Attachment{ FileType: imageFileHeader.Header.Get("Content-Type"), Filename: formatName("image.png"), } uploadFileArgs := mock.Arguments{ imageFileHeader, directory, mock.AnythingOfType("string"), attachment.FileType, } mockFileRepository := new(mocks.FileRepository) mockFileRepository. On("UploadFile", uploadFileArgs...). Run(func(args mock.Arguments) { attachment.ID = id }). Return(imageURL, nil) ms := NewMessageService(&MSConfig{ FileRepository: mockFileRepository, }) _, err := ms.UploadFile(imageFileHeader, channelId) assert.NoError(t, err) mockFileRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { channelId := fixture.RandID() multipartImageFixture := fixture.NewMultipartImage("image.png", "image/png") defer multipartImageFixture.Close() imageFileHeader := multipartImageFixture.GetFormFile() directory := fmt.Sprintf("channels/%s", channelId) attachment := model.Attachment{ FileType: imageFileHeader.Header.Get("Content-Type"), Filename: formatName("image.png"), } uploadFileArgs := mock.Arguments{ imageFileHeader, directory, mock.AnythingOfType("string"), attachment.FileType, } mockFileRepository := new(mocks.FileRepository) mockError := apperrors.NewInternal() mockFileRepository. On("UploadFile", uploadFileArgs...). Return("", mockError) ms := NewMessageService(&MSConfig{ FileRepository: mockFileRepository, }) att, err := ms.UploadFile(imageFileHeader, channelId) assert.Error(t, err) assert.Equal(t, err, apperrors.NewInternal()) assert.Nil(t, att) mockFileRepository.AssertExpectations(t) }) } ================================================ FILE: server/service/password.go ================================================ package service import ( "crypto/rand" "encoding/hex" "fmt" "strings" "golang.org/x/crypto/scrypt" ) // hashPassword hashes the given password using bcrypt func hashPassword(password string) (string, error) { salt := make([]byte, 32) _, err := rand.Read(salt) if err != nil { return "", err } // using recommended cost parameters from - https://godoc.org/golang.org/x/crypto/scrypt shash, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32) if err != nil { return "", err } // return hex-encoded string with salt appended to password hashedPW := fmt.Sprintf("%s.%s", hex.EncodeToString(shash), hex.EncodeToString(salt)) return hashedPW, nil } // comparePasswords compares the stored password with the supplied one func comparePasswords(storedPassword string, suppliedPassword string) (bool, error) { pwsalt := strings.Split(storedPassword, ".") if len(pwsalt) < 2 { return false, fmt.Errorf("did not provide a valid hash") } // check supplied password salted with hash salt, err := hex.DecodeString(pwsalt[1]) if err != nil { return false, fmt.Errorf("unable to verify user password") } shash, err := scrypt.Key([]byte(suppliedPassword), salt, 32768, 8, 1, 32) if err != nil { return false, fmt.Errorf("unable to verify user password") } return hex.EncodeToString(shash) == pwsalt[0], nil } ================================================ FILE: server/service/password_test.go ================================================ package service import ( "github.com/sentrionic/valkyrie/model/fixture" "testing" "github.com/stretchr/testify/require" ) func TestPassword(t *testing.T) { password := fixture.RandStringRunes(10) hashedPassword1, err := hashPassword(password) require.NoError(t, err) require.NotEmpty(t, hashedPassword1) valid, err := comparePasswords(hashedPassword1, password) require.NoError(t, err) require.True(t, valid) wrongPassword := fixture.RandStringRunes(10) valid, err = comparePasswords(hashedPassword1, wrongPassword) require.NoError(t, err) require.False(t, valid) hashedPassword2, err := hashPassword(password) require.NoError(t, err) require.NotEmpty(t, hashedPassword2) require.NotEqual(t, hashedPassword1, hashedPassword2) valid, err = comparePasswords(password, hashedPassword1) require.Error(t, err) require.EqualError(t, err, "did not provide a valid hash") require.False(t, valid) } ================================================ FILE: server/service/socket_service.go ================================================ package service import ( "encoding/json" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/ws" "log" ) type socketService struct { Hub ws.Hub GuildRepository model.GuildRepository ChannelRepository model.ChannelRepository } // SSConfig will hold repositories that will eventually be injected into // this service layer type SSConfig struct { Hub ws.Hub GuildRepository model.GuildRepository ChannelRepository model.ChannelRepository } // NewSocketService is a factory function for // initializing a SocketService with its repository layer dependencies func NewSocketService(c *SSConfig) model.SocketService { return &socketService{ Hub: c.Hub, GuildRepository: c.GuildRepository, ChannelRepository: c.ChannelRepository, } } func (s *socketService) EmitNewMessage(room string, message *model.MessageResponse) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.NewMessageAction, Data: message, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) } func (s *socketService) EmitEditMessage(room string, message *model.MessageResponse) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.EditMessageAction, Data: message, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) } func (s *socketService) EmitDeleteMessage(room, messageId string) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.DeleteMessageAction, Data: messageId, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) } func (s *socketService) EmitNewChannel(room string, channel *model.ChannelResponse) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.AddChannelAction, Data: channel, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) } func (s *socketService) EmitNewPrivateChannel(members []string, channel *model.ChannelResponse) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.AddPrivateChannelAction, Data: channel, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } for _, id := range members { s.Hub.BroadcastToRoom(data, id) } } func (s *socketService) EmitEditChannel(room string, channel *model.ChannelResponse) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.EditChannelAction, Data: channel, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) } func (s *socketService) EmitDeleteChannel(channel *model.Channel) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.DeleteChannelAction, Data: channel.ID, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, *channel.GuildID) } func (s *socketService) EmitEditGuild(guild *model.Guild) { response := guild.SerializeGuild("") data, err := json.Marshal(model.WebsocketMessage{ Action: ws.EditGuildAction, Data: response, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } members, err := s.GuildRepository.GetMemberIds(guild.ID) if err != nil { log.Printf("error getting member ids: %v\n", err) } for _, id := range *members { s.Hub.BroadcastToRoom(data, id) } } func (s *socketService) EmitDeleteGuild(guildId string, members []string) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.DeleteGuildAction, Data: guildId, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } for _, id := range members { s.Hub.BroadcastToRoom(data, id) } } func (s *socketService) EmitRemoveFromGuild(memberId, guildId string) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.RemoveFromGuildAction, Data: guildId, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, memberId) } func (s *socketService) EmitAddMember(room string, member *model.User) { response := model.MemberResponse{ Id: member.ID, Username: member.Username, Image: member.Image, IsOnline: member.IsOnline, CreatedAt: member.CreatedAt, UpdatedAt: member.UpdatedAt, IsFriend: false, } data, err := json.Marshal(model.WebsocketMessage{ Action: ws.AddMemberAction, Data: response, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) } func (s *socketService) EmitRemoveMember(room, memberId string) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.RemoveMemberAction, Data: memberId, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) } func (s *socketService) EmitNewDMNotification(channelId string, user *model.User) { response := model.DirectMessage{ Id: channelId, User: model.DMUser{ Id: user.ID, Username: user.Username, Image: user.Image, IsOnline: user.IsOnline, IsFriend: false, }, } notification, err := json.Marshal(model.WebsocketMessage{ Action: ws.NewDMNotificationAction, Data: response, }) if err != nil { log.Printf("error marshalling notification: %v\n", err) } pushToTop, err := json.Marshal(model.WebsocketMessage{ Action: ws.PushToTopAction, Data: channelId, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } members, err := s.ChannelRepository.GetDMMemberIds(channelId) if err != nil { log.Printf("error getting member ids: %v\n", err) } for _, id := range *members { if id != user.ID { s.Hub.BroadcastToRoom(notification, id) } s.Hub.BroadcastToRoom(pushToTop, id) } } func (s *socketService) EmitNewNotification(guildId, channelId string) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.NewNotificationAction, Data: guildId, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } members, err := s.GuildRepository.GetMemberIds(guildId) if err != nil { log.Printf("error getting member ids: %v\n", err) } for _, id := range *members { s.Hub.BroadcastToRoom(data, id) } notification, err := json.Marshal(model.WebsocketMessage{ Action: ws.NewNotificationAction, Data: channelId, }) if err != nil { log.Printf("error marshalling notification: %v\n", err) } s.Hub.BroadcastToRoom(notification, guildId) } func (s *socketService) EmitSendRequest(room string) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.SendRequestAction, Data: "", }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) } func (s *socketService) EmitAddFriendRequest(room string, request *model.FriendRequest) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.AddRequestAction, Data: request, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, room) s.EmitSendRequest(room) } func (s *socketService) EmitAddFriend(user, member *model.User) { userResponse := model.Friend{ Id: user.ID, Username: user.Username, Image: user.Image, IsOnline: user.IsOnline, } data, err := json.Marshal(model.WebsocketMessage{ Action: ws.AddFriendAction, Data: userResponse, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, member.ID) memberResponse := model.Friend{ Id: member.ID, Username: member.Username, Image: member.Image, IsOnline: member.IsOnline, } data, err = json.Marshal(model.WebsocketMessage{ Action: ws.AddFriendAction, Data: memberResponse, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, user.ID) } func (s *socketService) EmitRemoveFriend(userId, memberId string) { data, err := json.Marshal(model.WebsocketMessage{ Action: ws.RemoveFriendAction, Data: memberId, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, userId) data, err = json.Marshal(model.WebsocketMessage{ Action: ws.RemoveFriendAction, Data: userId, }) if err != nil { log.Printf("error marshalling response: %v\n", err) } s.Hub.BroadcastToRoom(data, memberId) } ================================================ FILE: server/service/user_service.go ================================================ package service import ( "context" "crypto/md5" "encoding/hex" "fmt" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "log" "mime/multipart" "strings" ) // UserService acts as a struct for injecting an implementation of UserRepository // for use in service methods type userService struct { UserRepository model.UserRepository FileRepository model.FileRepository RedisRepository model.RedisRepository MailRepository model.MailRepository } // USConfig will hold repositories that will eventually be injected into // this service layer type USConfig struct { UserRepository model.UserRepository FileRepository model.FileRepository RedisRepository model.RedisRepository MailRepository model.MailRepository } // NewUserService is a factory function for // initializing a UserService with its repository layer dependencies func NewUserService(c *USConfig) model.UserService { return &userService{ UserRepository: c.UserRepository, FileRepository: c.FileRepository, RedisRepository: c.RedisRepository, MailRepository: c.MailRepository, } } // Get retrieves a user based on their uid func (s *userService) Get(uid string) (*model.User, error) { return s.UserRepository.FindByID(uid) } // GetByEmail retrieves a user based on their email func (s *userService) GetByEmail(email string) (*model.User, error) { // Sanitize email email = strings.ToLower(email) email = strings.TrimSpace(email) return s.UserRepository.FindByEmail(email) } // Register creates a user func (s *userService) Register(user *model.User) (*model.User, error) { hashedPassword, err := hashPassword(user.Password) if err != nil { log.Printf("Unable to signup user for email: %v\n", user.Email) return nil, apperrors.NewInternal() } user.ID = GenerateId() user.Image = generateAvatar(user.Email) user.Password = hashedPassword return s.UserRepository.Create(user) } // Login reaches out to the UserRepository check if the user exists // and then compares the supplied password with the provided password // if a valid email/password combo is provided, u will hold all // available user fields func (s *userService) Login(email, password string) (*model.User, error) { user, err := s.UserRepository.FindByEmail(email) // Will return NotAuthorized to client to omit details of why if err != nil { return nil, apperrors.NewAuthorization(apperrors.InvalidCredentials) } // verify match, err := comparePasswords(user.Password, password) if err != nil { return nil, apperrors.NewInternal() } if !match { return nil, apperrors.NewAuthorization(apperrors.InvalidCredentials) } return user, nil } func (s *userService) UpdateAccount(u *model.User) error { return s.UserRepository.Update(u) } func (s *userService) IsEmailAlreadyInUse(email string) bool { user, err := s.UserRepository.FindByEmail(email) if err != nil { return true } return user.ID != "" } func (s *userService) ChangeAvatar(header *multipart.FileHeader, directory string) (string, error) { return s.FileRepository.UploadAvatar(header, directory) } func (s *userService) DeleteImage(key string) error { return s.FileRepository.DeleteImage(key) } func (s *userService) ChangePassword(currentPassword, newPassword string, user *model.User) error { // verify match, err := comparePasswords(user.Password, currentPassword) if err != nil { return apperrors.NewInternal() } if !match { return apperrors.NewAuthorization(apperrors.InvalidOldPassword) } hashedPassword, err := hashPassword(newPassword) if err != nil { log.Printf("Unable to change password for email: %v\n", user.Email) return apperrors.NewInternal() } user.Password = hashedPassword return s.UserRepository.Update(user) } func (s *userService) ForgotPassword(ctx context.Context, user *model.User) error { token, err := s.RedisRepository.SetResetToken(ctx, user.ID) if err != nil { return err } return s.MailRepository.SendResetMail(user.Email, token) } func (s *userService) ResetPassword(ctx context.Context, password string, token string) (*model.User, error) { id, err := s.RedisRepository.GetIdFromToken(ctx, token) if err != nil { return nil, err } user, err := s.UserRepository.FindByID(id) if err != nil { return nil, err } hashedPassword, err := hashPassword(password) if err != nil { log.Printf("Unable to reset password") return nil, apperrors.NewInternal() } user.Password = hashedPassword if err = s.UserRepository.Update(user); err != nil { return nil, err } return user, nil } func (s *userService) GetFriendAndGuildIds(userId string) (*[]string, error) { return s.UserRepository.GetFriendAndGuildIds(userId) } func (s *userService) GetRequestCount(userId string) (*int64, error) { return s.UserRepository.GetRequestCount(userId) } // generateAvatar returns a gravatar using the md5 hash of the email func generateAvatar(email string) string { hash := md5.Sum([]byte(email)) return fmt.Sprintf("https://gravatar.com/avatar/%s?d=identicon", hex.EncodeToString(hash[:])) } ================================================ FILE: server/service/user_service_test.go ================================================ package service import ( "context" "fmt" "github.com/sentrionic/valkyrie/mocks" "github.com/sentrionic/valkyrie/model" "github.com/sentrionic/valkyrie/model/apperrors" "github.com/sentrionic/valkyrie/model/fixture" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "testing" ) func TestGet(t *testing.T) { t.Run("Success", func(t *testing.T) { uid := GenerateId() mockUser := fixture.GetMockUser() mockUser.ID = uid mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, }) mockUserRepository.On("FindByID", uid).Return(mockUser, nil) u, err := us.Get(uid) assert.NoError(t, err) assert.Equal(t, u, mockUser) mockUserRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { uid := GenerateId() mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, }) mockUserRepository.On("FindByID", uid).Return(nil, fmt.Errorf("some error down the call chain")) u, err := us.Get(uid) assert.Nil(t, u) assert.Error(t, err) mockUserRepository.AssertExpectations(t) }) } func TestUserService_GetByEmail(t *testing.T) { t.Run("Success", func(t *testing.T) { mockUser := fixture.GetMockUser() mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, }) mockUserRepository.On("FindByEmail", mockUser.Email).Return(mockUser, nil) u, err := us.GetByEmail(mockUser.Email) assert.NoError(t, err) assert.Equal(t, u, mockUser) mockUserRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { email := fixture.Email() mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, }) mockUserRepository.On("FindByEmail", email).Return(nil, fmt.Errorf("some error down the call chain")) u, err := us.GetByEmail(email) assert.Nil(t, u) assert.Error(t, err) mockUserRepository.AssertExpectations(t) }) } func TestRegister(t *testing.T) { t.Run("Success", func(t *testing.T) { uid := GenerateId() mockUser := fixture.GetMockUser() initial := &model.User{ Username: mockUser.Username, Email: mockUser.Email, Password: mockUser.Password, } mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, }) // We can use Run method to modify the user when the Create method is called. // We can then chain on a Return method to return no error mockUserRepository. On("Create", initial). Run(func(args mock.Arguments) { mockUser.ID = uid }).Return(mockUser, nil) user, err := us.Register(initial) assert.NoError(t, err) // assert user now has a userID assert.Equal(t, uid, mockUser.ID) assert.Equal(t, user, mockUser) mockUserRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockUser := &model.User{ Email: "bob@bob.com", Username: "bobby", Password: "howdyhoneighbor!", } mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, }) mockErr := apperrors.NewConflict("email", "bob@bob.com") // We can use Run method to modify the user when the Create method is called. // We can then chain on a Return method to return no error mockUserRepository. On("Create", mockUser). Return(nil, mockErr) user, err := us.Register(mockUser) // assert error is error we response with in mock assert.EqualError(t, err, mockErr.Error()) assert.Nil(t, user) mockUserRepository.AssertExpectations(t) }) } func TestLogin(t *testing.T) { // setup valid email/pw combo with hashed password to test method // response when provided password is invalid validPW := "howdyhoneighbor!" hashedValidPW, _ := hashPassword(validPW) invalidPW := "howdyhodufus!" mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, }) t.Run("Success", func(t *testing.T) { mockUser := fixture.GetMockUser() mockUser.Password = hashedValidPW mockUserRepository. On("FindByEmail", mockUser.Email).Return(mockUser, nil) user, err := us.Login(mockUser.Email, validPW) assert.NoError(t, err) assert.Equal(t, user, mockUser) mockUserRepository.AssertCalled(t, "FindByEmail", mockUser.Email) }) t.Run("Invalid email/password combination", func(t *testing.T) { uid := GenerateId() mockUserResp := fixture.GetMockUser() mockUserResp.ID = uid mockUserResp.Password = hashedValidPW mockArgs := mock.Arguments{ mockUserResp.Email, } // We can use Run method to modify the user when the Create method is called. // We can then chain on a Return method to return no error mockUserRepository. On("FindByEmail", mockArgs...).Return(mockUserResp, nil) user, err := us.Login(mockUserResp.Email, invalidPW) assert.Error(t, err) assert.EqualError(t, err, apperrors.InvalidCredentials) assert.Nil(t, user) mockUserRepository.AssertCalled(t, "FindByEmail", mockArgs...) }) } func TestUpdateDetails(t *testing.T) { mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, }) t.Run("Success", func(t *testing.T) { uid := GenerateId() mockUser := fixture.GetMockUser() mockUser.ID = uid mockArgs := mock.Arguments{ mockUser, } mockUserRepository. On("Update", mockArgs...).Return(nil) err := us.UpdateAccount(mockUser) assert.NoError(t, err) mockUserRepository.AssertCalled(t, "Update", mockArgs...) }) t.Run("Failure", func(t *testing.T) { uid := GenerateId() mockUser := fixture.GetMockUser() mockUser.ID = uid mockArgs := mock.Arguments{ mockUser, } mockError := apperrors.NewInternal() mockUserRepository. On("Update", mockArgs...).Return(mockError) err := us.UpdateAccount(mockUser) assert.Error(t, err) apperror, ok := err.(*apperrors.Error) assert.True(t, ok) assert.Equal(t, apperrors.Internal, apperror.Type) mockUserRepository.AssertCalled(t, "Update", mockArgs...) }) } func TestUserService_ChangeAvatar(t *testing.T) { mockUserRepository := new(mocks.UserRepository) mockFileRepository := new(mocks.FileRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, FileRepository: mockFileRepository, }) t.Run("Successful new image", func(t *testing.T) { uid := GenerateId() // does not have have imageURL mockUser := fixture.GetMockUser() mockUser.ID = uid mockUser.Image = "" multipartImageFixture := fixture.NewMultipartImage("image.png", "image/png") defer multipartImageFixture.Close() imageFileHeader := multipartImageFixture.GetFormFile() directory := "test_dir" uploadFileArgs := mock.Arguments{ imageFileHeader, directory, } imageURL := "https://imageurl.com/jdfkj34kljl" mockFileRepository. On("UploadAvatar", uploadFileArgs...). Return(imageURL, nil) updateArgs := mock.Arguments{ mockUser, } mockUpdatedUser := &model.User{ BaseModel: model.BaseModel{ ID: mockUser.ID, CreatedAt: mockUser.CreatedAt, UpdatedAt: mockUser.UpdatedAt, }, Email: mockUser.Email, Username: mockUser.Username, Image: imageURL, Password: mockUser.Password, } mockUserRepository. On("Update", updateArgs...). Return(nil) url, err := us.ChangeAvatar(imageFileHeader, directory) assert.NoError(t, err) mockUser.Image = url err = us.UpdateAccount(mockUser) assert.NoError(t, err) assert.Equal(t, mockUpdatedUser, mockUser) mockFileRepository.AssertCalled(t, "UploadAvatar", uploadFileArgs...) mockUserRepository.AssertCalled(t, "Update", updateArgs...) }) t.Run("Successful update image", func(t *testing.T) { imageURL := "https://imageurl.com/jdfkj34kljl" uid := GenerateId() mockUser := &model.User{ Email: "new@bob.com", Username: "NewRobert", Image: imageURL, } mockUser.ID = uid multipartImageFixture := fixture.NewMultipartImage("image.png", "image/png") defer multipartImageFixture.Close() imageFileHeader := multipartImageFixture.GetFormFile() directory := "test_dir" uploadFileArgs := mock.Arguments{ imageFileHeader, directory, } deleteImageArgs := mock.Arguments{ imageURL, } mockFileRepository. On("UploadAvatar", uploadFileArgs...). Return(imageURL, nil) mockFileRepository. On("DeleteImage", deleteImageArgs...). Return(nil) mockUpdatedUser := &model.User{ Email: "new@bob.com", Username: "NewRobert", Image: imageURL, } mockUpdatedUser.ID = uid updateArgs := mock.Arguments{ mockUser, } mockUserRepository. On("Update", updateArgs...). Return(nil) url, err := us.ChangeAvatar(imageFileHeader, directory) assert.NoError(t, err) err = us.DeleteImage(mockUser.Image) assert.NoError(t, err) mockUser.Image = url err = us.UpdateAccount(mockUser) assert.NoError(t, err) assert.Equal(t, mockUpdatedUser, mockUser) mockFileRepository.AssertCalled(t, "UploadAvatar", uploadFileArgs...) mockFileRepository.AssertCalled(t, "DeleteImage", imageURL) mockUserRepository.AssertCalled(t, "Update", updateArgs...) }) t.Run("FileRepository Error", func(t *testing.T) { // need to create a new UserService and repository // because testify has no way to overwrite a mock's // "On" call. mockUserRepository := new(mocks.UserRepository) mockFileRepository := new(mocks.FileRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, FileRepository: mockFileRepository, }) multipartImageFixture := fixture.NewMultipartImage("image.png", "image/png") defer multipartImageFixture.Close() imageFileHeader := multipartImageFixture.GetFormFile() directory := "file_directory" uploadFileArgs := mock.Arguments{ imageFileHeader, directory, } mockError := apperrors.NewInternal() mockFileRepository. On("UploadAvatar", uploadFileArgs...). Return("", mockError) url, err := us.ChangeAvatar(imageFileHeader, directory) assert.Equal(t, "", url) assert.Error(t, err) mockFileRepository.AssertCalled(t, "UploadAvatar", uploadFileArgs...) mockUserRepository.AssertNotCalled(t, "Update") }) t.Run("UserRepository UpdateImage Error", func(t *testing.T) { uid := GenerateId() imageURL := "https://imageurl.com/jdfkj34kljl" // has imageURL mockUser := &model.User{ Email: "new@bob.com", Username: "A New Bob!", Image: imageURL, } mockUser.ID = uid multipartImageFixture := fixture.NewMultipartImage("image.png", "image/png") defer multipartImageFixture.Close() imageFileHeader := multipartImageFixture.GetFormFile() directory := "file_dir" uploadFileArgs := mock.Arguments{ imageFileHeader, directory, } mockFileRepository. On("UploadAvatar", uploadFileArgs...). Return(imageURL, nil) updateArgs := mock.Arguments{ mockUser, } mockError := apperrors.NewInternal() mockUserRepository. On("Update", updateArgs...). Return(mockError) url, err := us.ChangeAvatar(imageFileHeader, directory) assert.NoError(t, err) assert.Equal(t, imageURL, url) err = us.UpdateAccount(mockUser) assert.Error(t, err) mockFileRepository.AssertCalled(t, "UploadAvatar", uploadFileArgs...) mockUserRepository.AssertCalled(t, "Update", updateArgs...) }) } func TestUserService_ChangePassword(t *testing.T) { t.Run("Success", func(t *testing.T) { mockUser := fixture.GetMockUser() currentPassword := mockUser.Password hashedPassword, err := hashPassword(currentPassword) assert.NoError(t, err) mockUser.Password = hashedPassword newPassword := fixture.RandStringRunes(10) mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{UserRepository: mockUserRepository}) mockUserRepository.On("Update", mockUser).Return(nil) err = us.ChangePassword(currentPassword, newPassword, mockUser) assert.NoError(t, err) assert.NotEqual(t, mockUser.Password, newPassword) assert.NotEqual(t, mockUser.Password, hashedPassword) mockUserRepository.AssertExpectations(t) }) t.Run("Error verifying password", func(t *testing.T) { mockUser := fixture.GetMockUser() currentPassword := mockUser.Password newPassword := fixture.RandStringRunes(10) mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{UserRepository: mockUserRepository}) err := us.ChangePassword(currentPassword, newPassword, mockUser) assert.Error(t, err) assert.Equal(t, err, apperrors.NewInternal()) mockUserRepository.AssertNotCalled(t, "Update") }) t.Run("Current Password is incorrect", func(t *testing.T) { mockUser := fixture.GetMockUser() currentPassword := fixture.RandStringRunes(10) hashedPassword, err := hashPassword(mockUser.Password) assert.NoError(t, err) mockUser.Password = hashedPassword newPassword := fixture.RandStringRunes(10) mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{UserRepository: mockUserRepository}) err = us.ChangePassword(currentPassword, newPassword, mockUser) assert.Error(t, err) assert.Equal(t, err, apperrors.NewAuthorization(apperrors.InvalidOldPassword)) mockUserRepository.AssertNotCalled(t, "Update") }) t.Run("Error returned from the repository", func(t *testing.T) { mockUser := fixture.GetMockUser() currentPassword := mockUser.Password hashedPassword, err := hashPassword(currentPassword) assert.NoError(t, err) mockUser.Password = hashedPassword newPassword := fixture.RandStringRunes(10) mockUserRepository := new(mocks.UserRepository) us := NewUserService(&USConfig{UserRepository: mockUserRepository}) mockError := apperrors.NewInternal() mockUserRepository.On("Update", mockUser).Return(mockError) err = us.ChangePassword(currentPassword, newPassword, mockUser) assert.Error(t, err) mockUserRepository.AssertExpectations(t) }) } func TestUserService_ForgotPassword(t *testing.T) { mockUser := fixture.GetMockUser() token := fixture.RandStringRunes(10) t.Run("Success", func(t *testing.T) { mockRedisRepository := new(mocks.RedisRepository) mockMailRepository := new(mocks.MailRepository) us := NewUserService(&USConfig{ RedisRepository: mockRedisRepository, MailRepository: mockMailRepository, }) mockRedisRepository.On("SetResetToken", mock.Anything, mockUser.ID).Return(token, nil) mockMailRepository.On("SendResetMail", mockUser.Email, token).Return(nil) err := us.ForgotPassword(context.TODO(), mockUser) assert.NoError(t, err) mockRedisRepository.AssertExpectations(t) mockMailRepository.AssertExpectations(t) }) t.Run("Error", func(t *testing.T) { mockRedisRepository := new(mocks.RedisRepository) mockMailRepository := new(mocks.MailRepository) us := NewUserService(&USConfig{ RedisRepository: mockRedisRepository, MailRepository: mockMailRepository, }) mockError := apperrors.NewInternal() mockRedisRepository.On("SetResetToken", mock.Anything, mockUser.ID).Return("", mockError) err := us.ForgotPassword(context.TODO(), mockUser) assert.Error(t, err) mockRedisRepository.AssertExpectations(t) mockMailRepository.AssertNotCalled(t, "SendResetMail") }) } func TestUserService_ResetPassword(t *testing.T) { mockUser := fixture.GetMockUser() password := fixture.RandStr(10) token := fixture.RandStr(10) t.Run("Success", func(t *testing.T) { mockUserRepository := new(mocks.UserRepository) mockRedisRepository := new(mocks.RedisRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, RedisRepository: mockRedisRepository, }) mockRedisRepository.On("GetIdFromToken", mock.Anything, token).Return(mockUser.ID, nil) mockUserRepository.On("FindByID", mockUser.ID).Return(mockUser, nil) mockUserRepository.On("Update", mockUser).Return(nil) user, err := us.ResetPassword(context.TODO(), password, token) assert.NoError(t, err) assert.NotNil(t, user) mockUserRepository.AssertExpectations(t) mockRedisRepository.AssertExpectations(t) }) t.Run("No id found", func(t *testing.T) { mockUserRepository := new(mocks.UserRepository) mockRedisRepository := new(mocks.RedisRepository) us := NewUserService(&USConfig{ UserRepository: mockUserRepository, RedisRepository: mockRedisRepository, }) mockError := apperrors.NewInternal() mockRedisRepository.On("GetIdFromToken", mock.Anything, token).Return("", mockError) user, err := us.ResetPassword(context.TODO(), password, token) assert.Error(t, err) assert.Nil(t, user) mockRedisRepository.AssertCalled(t, "GetIdFromToken", mock.Anything, token) mockUserRepository.AssertNotCalled(t, "FindByID") mockUserRepository.AssertNotCalled(t, "Update") }) t.Run("No user found", func(t *testing.T) { mockUserRepository := new(mocks.UserRepository) mockRedisRepository := new(mocks.RedisRepository) id := fixture.RandID() us := NewUserService(&USConfig{ UserRepository: mockUserRepository, RedisRepository: mockRedisRepository, }) mockError := apperrors.NewInternal() mockRedisRepository.On("GetIdFromToken", mock.Anything, token).Return(id, nil) mockUserRepository.On("FindByID", id).Return(nil, mockError) user, err := us.ResetPassword(context.TODO(), password, token) assert.Error(t, err) assert.Nil(t, user) mockRedisRepository.AssertCalled(t, "GetIdFromToken", mock.Anything, token) mockUserRepository.AssertCalled(t, "FindByID", id) mockUserRepository.AssertNotCalled(t, "Update") }) t.Run("Error returned from the repository", func(t *testing.T) { mockUserRepository := new(mocks.UserRepository) mockRedisRepository := new(mocks.RedisRepository) mockError := apperrors.NewInternal() us := NewUserService(&USConfig{ UserRepository: mockUserRepository, RedisRepository: mockRedisRepository, }) mockRedisRepository.On("GetIdFromToken", mock.Anything, token).Return(mockUser.ID, nil) mockUserRepository.On("FindByID", mockUser.ID).Return(mockUser, nil) mockUserRepository.On("Update", mockUser).Return(mockError) user, err := us.ResetPassword(context.TODO(), password, token) assert.Error(t, err) assert.Nil(t, user) mockUserRepository.AssertExpectations(t) mockRedisRepository.AssertExpectations(t) }) } ================================================ FILE: server/static/asyncapi.yml ================================================ asyncapi: '2.4.0' info: title: Valkyrie Websockets version: '1.0.0' description: > This service is in charge of processing websocket events. Websockets are authenticated using sessions. All received messages must be specified like this: | { "action": "joinRoom", "room": "123456789", "message": "username"} |. Room is required to join a channel room, message can be used for additional arguments or information. Both are optional. Emited messages are of form | { "action": "new_message", "data": object } |. servers: production: url: wss://api.valkyrieapp.xyz/ws protocol: wss development: url: ws://localhost:4000/ws protocol: ws channels: /: publish: message: oneOf: - $ref: '#/components/messages/toggleOnline' - $ref: '#/components/messages/toggleOffline' - $ref: '#/components/messages/joinUser' - $ref: '#/components/messages/joinChannel' - $ref: '#/components/messages/joinGuild' - $ref: '#/components/messages/startTyping' - $ref: '#/components/messages/stopTyping' - $ref: '#/components/messages/getRequestCount' - $ref: '#/components/messages/leaveGuild' - $ref: '#/components/messages/leaveRoom' subscribe: message: oneOf: - $ref: '#/components/messages/addChannel' - $ref: '#/components/messages/deleteChannel' - $ref: '#/components/messages/editChannel' - $ref: '#/components/messages/editGuild' - $ref: '#/components/messages/deleteGuild' - $ref: '#/components/messages/addMember' - $ref: '#/components/messages/removeMember' - $ref: '#/components/messages/new_message' - $ref: '#/components/messages/edit_message' - $ref: '#/components/messages/delete_message' - $ref: '#/components/messages/push_to_top' - $ref: '#/components/messages/new_notification' - $ref: '#/components/messages/toggle_online' - $ref: '#/components/messages/toggle_offline' - $ref: '#/components/messages/addToTyping' - $ref: '#/components/messages/removeFromTyping' - $ref: '#/components/messages/send_request' - $ref: '#/components/messages/add_friend' - $ref: '#/components/messages/remove_friend' - $ref: '#/components/messages/requestCount' components: securitySchemes: session: type: httpApiKey name: token in: query schemas: attachment: type: object properties: fallback: type: string color: type: string pretext: type: string author_name: type: string author_link: type: string format: uri author_icon: type: string format: uri title: type: string title_link: type: string format: uri text: type: string fields: type: array items: type: object properties: title: type: string value: type: string short: type: boolean image_url: type: string format: uri thumb_url: type: string format: uri footer: type: string footer_icon: type: string format: uri ts: type: number messages: addChannel: summary: 'A channel was created.' payload: type: object description: 'see ChannelResponse' properties: id: type: string name: type: string isPublic: type: boolean createdAt: type: string updatedAt: type: string hasNotification: type: boolean deleteChannel: summary: 'A channel was deleted.' payload: type: string properties: id: type: string editChannel: summary: 'A channel was edited' payload: type: object description: 'see ChannelResponse' properties: id: type: string name: type: string isPublic: type: boolean createdAt: type: string updatedAt: type: string hasNotification: type: boolean editGuild: summary: 'A guild was edited' payload: type: object description: 'see Guild' properties: name: type: string icon: type: string deleteGuild: summary: 'A guild was deleted.' payload: type: string properties: id: type: string addMember: summary: 'A member got added to the guild.' payload: type: object description: 'see MemberResponse' properties: id: type: string username: type: string image: type: string isOnline: type: string createdAt: type: string updatedAt: type: string isFriend: type: string removeMember: summary: 'A member was removed from the guild.' payload: type: string properties: id: type: string new_message: summary: 'A new message was sent to a channel.' payload: type: object properties: user: type: object description: see MemberResponse id: type: string text: type: string url: type: string filetype: type: string createdAt: type: string updatedAt: type: string edit_message: summary: 'A message in this channel was edited.' payload: type: object properties: user: type: object description: see MemberResponse id: type: string text: type: string url: type: string filetype: type: string createdAt: type: string updatedAt: type: string delete_message: summary: 'A message in this channel was deleted.' payload: type: string properties: id: type: string push_to_top: summary: 'A notification that pushes the DM to the top of the list.' payload: type: string properties: dmChannelId: type: string toggle_online: summary: 'A notification that the user went online. Gets emited to guild members that currently view the guild and friends of the user.' payload: type: string properties: userId: type: string toggle_offline: summary: 'A notification that the user went offline. Gets emited to guild members that currently view the guild and friends of the user.' payload: type: string properties: userId: type: string new_notification: summary: 'A new message notification, published to all guild members. Additionally sends the channelId to members that currently view the guild.' payload: type: string properties: channelId: type: string guildId: type: string addToTyping: summary: 'Emits the username to the channel the user is currently typing in.' payload: type: string properties: username: type: string removeFromTyping: summary: 'Emits the username to the channel the user was typing in.' payload: type: string properties: username: type: string send_request: summary: 'Emits a notification that a friends request was received' add_friend: summary: 'Adds the added person to the friends list.' payload: type: object description: 'see MemberResponse' properties: member: type: object remove_friend: summary: 'Removes the former friend from the friends list.' payload: type: string properties: id: type: string requestCount: summary: 'The amount of friends requests the user has' payload: type: number properties: count: type: number toggleOnline: summary: 'Changes the users status to online and broadcasts it to all friends and guilds they are part of.' toggleOffline: summary: 'Changes the users status to offline and broadcasts it to all friends and guilds they are part of. Leaves all connected rooms.' joinUser: summary: 'Joins the users room. This room receives guild, DM & friend notifications' payload: type: string properties: userId: type: string joinChannel: summary: 'Joins the channels room. Checks if the user is a member of said channel. Receives message & typing events.' payload: type: string properties: channelId: type: string joinGuild: summary: 'Joins the guilds room. Requires member access. Receives guild member & channel events.' payload: type: string properties: guildId: type: string startTyping: summary: 'Emits the username to the channel they are typing in.' payload: type: string properties: channelId: type: string username: type: string stopTyping: summary: 'Removes the username from the channel they were typing in.' payload: type: string properties: channelId: type: string username: type: string getRequestCount: summary: 'Gets the amount of friend requests the user has.' leaveGuild: summary: 'Leaves the guild room.' payload: type: string properties: guildId: type: string leaveRoom: summary: 'Leaves the room.' payload: type: string properties: roomId: type: string ================================================ FILE: server/static/index.html ================================================ Valkyrie Websockets <span>1.0.0</span> documentation

Valkyrie Websockets 1.0.0

This service is in charge of processing websocket events. Websockets are authenticated using sessions. All messages send to the server must be specified like this:

{
  "action": "joinRoom",
  "room": "123456789",
  "message": "username"
}
Room is required to join a channel room, message can be used for additional arguments or information. Both are optional. Messages emitted from the server are of form:
{
  "action": "new_message",
  "message": {
    ... properties
  }
}

Servers

wss://api.valkyrieapp.xyz/ws wss
ws://localhost:8080/ws ws

Operations

Pub /

Accepts one of the following messages:

#1 toggleOnline

Changes the users status to online and broadcasts it to all friends and guilds they are part of.

#2 toggleOffline

Changes the users status to offline and broadcasts it to all friends and guilds they are part of. Leaves all connected rooms.

#3 joinUser

Joins the users room. This room receives guild, DM & friend notifications

Payload
string
userId
string
#4 joinChannel

Joins the channels room. Checks if the user is a member of said channel. Receives message & typing events.

Payload
string
channelId
string
#5 joinGuild

Joins the guilds room. Requires member access. Receives guild member & channel events.

Payload
string
guildId
string
#6 startTyping

Emits the username to the channel they are typing in.

Payload
string
channelId
string
username
string
#7 stopTyping

Removes the username from the channel they were typing in.

Payload
string
channelId
string
username
string
#8 getRequestCount

Gets the amount of friend requests the user has.

#9 leaveGuild

Leaves the guild room.

Payload
string
guildId
string
#10 leaveRoom

Leaves the room.

Payload
string
roomId
string

Examples

toggleOnline
toggleOffline
joinUser
Payload
"string"
This example has been generated automatically.
joinChannel
Payload
"string"
This example has been generated automatically.
joinGuild
Payload
"string"
This example has been generated automatically.
startTyping
Payload
"string"
This example has been generated automatically.
stopTyping
Payload
"string"
This example has been generated automatically.
getRequestCount
leaveGuild
Payload
"string"
This example has been generated automatically.
leaveRoom
Payload
"string"
This example has been generated automatically.

Sub /

Accepts one of the following messages:

#1 addChannel

A channel was created.

Payload
object

see ChannelResponse

id
string
name
string
isPublic
boolean
createdAt
string
updatedAt
string
hasNotification
boolean

Additional properties are allowed.

#2 deleteChannel

A channel was deleted.

Payload
string
id
string
#3 editChannel

A channel was edited

Payload
object

see ChannelResponse

id
string
name
string
isPublic
boolean
createdAt
string
updatedAt
string
hasNotification
boolean

Additional properties are allowed.

#4 editGuild

A guild was edited

Payload
object

see Guild

name
string
icon
string

Additional properties are allowed.

#5 deleteGuild

A guild was deleted.

Payload
string
id
string
#6 addMember

A member got added to the guild.

Payload
object

see MemberResponse

id
string
username
string
image
string
isOnline
string
createdAt
string
updatedAt
string
isFriend
string

Additional properties are allowed.

#7 removeMember

A member was removed from the guild.

Payload
string
id
string
#8 new_message

A new message was sent to a channel.

Payload
object
user
object

see MemberResponse

Additional properties are allowed.

id
string
text
string
url
string
filetype
string
createdAt
string
updatedAt
string

Additional properties are allowed.

#9 edit_message

A message in this channel was edited.

Payload
object
user
object

see MemberResponse

Additional properties are allowed.

id
string
text
string
url
string
filetype
string
createdAt
string
updatedAt
string

Additional properties are allowed.

#10 delete_message

A message in this channel was deleted.

Payload
string
id
string
#11 push_to_top

A notification that pushes the DM to the top of the list.

Payload
string
dmChannelId
string
#12 new_notification

A new message notification, published to all guild members. Additionally sends the channelId to members that currently view the guild.

Payload
string
channelId
string
guildId
string
#13 toggle_online

A notification that the user went online. Gets emited to guild members that currently view the guild and friends of the user.

Payload
string
userId
string
#14 toggle_offline

A notification that the user went offline. Gets emited to guild members that currently view the guild and friends of the user.

Payload
string
userId
string
#15 addToTyping

Emits the username to the channel the user is currently typing in.

Payload
string
username
string
#16 removeFromTyping

Emits the username to the channel the user was typing in.

Payload
string
username
string
#17 send_request

Emits a notification that a friends request was received

#18 add_friend

Adds the added person to the friends list.

Payload
object

see MemberResponse

member
object

Additional properties are allowed.

Additional properties are allowed.

#19 remove_friend

Removes the former friend from the friends list.

Payload
string
id
string
#20 requestCount

The amount of friends requests the user has

Payload
number
count
number

Examples

addChannel
Payload
{
  "id": "string",
  "name": "string",
  "isPublic": true,
  "createdAt": "string",
  "updatedAt": "string",
  "hasNotification": true
}
This example has been generated automatically.
deleteChannel
Payload
"string"
This example has been generated automatically.
editChannel
Payload
{
  "id": "string",
  "name": "string",
  "isPublic": true,
  "createdAt": "string",
  "updatedAt": "string",
  "hasNotification": true
}
This example has been generated automatically.
editGuild
Payload
{
  "name": "string",
  "icon": "string"
}
This example has been generated automatically.
deleteGuild
Payload
"string"
This example has been generated automatically.
addMember
Payload
{
  "id": "string",
  "username": "string",
  "image": "string",
  "isOnline": "string",
  "createdAt": "string",
  "updatedAt": "string",
  "isFriend": "string"
}
This example has been generated automatically.
removeMember
Payload
"string"
This example has been generated automatically.
new_message
Payload
{
  "user": {},
  "id": "string",
  "text": "string",
  "url": "string",
  "filetype": "string",
  "createdAt": "string",
  "updatedAt": "string"
}
This example has been generated automatically.
edit_message
Payload
{
  "user": {},
  "id": "string",
  "text": "string",
  "url": "string",
  "filetype": "string",
  "createdAt": "string",
  "updatedAt": "string"
}
This example has been generated automatically.
delete_message
Payload
"string"
This example has been generated automatically.
push_to_top
Payload
"string"
This example has been generated automatically.
new_notification
Payload
"string"
This example has been generated automatically.
toggle_online
Payload
"string"
This example has been generated automatically.
toggle_offline
Payload
"string"
This example has been generated automatically.
addToTyping
Payload
"string"
This example has been generated automatically.
removeFromTyping
Payload
"string"
This example has been generated automatically.
send_request
add_friend
Payload
{
  "member": {}
}
This example has been generated automatically.
remove_friend
Payload
"string"
This example has been generated automatically.
requestCount
Payload
""
This example has been generated automatically.

Messages

#1 toggleOnline

Changes the users status to online and broadcasts it to all friends and guilds they are part of.

#2 toggleOffline

Changes the users status to offline and broadcasts it to all friends and guilds they are part of. Leaves all connected rooms.

#3 joinUser

Joins the users room. This room receives guild, DM & friend notifications

Payload
string
userId
string
#4 joinChannel

Joins the channels room. Checks if the user is a member of said channel. Receives message & typing events.

Payload
string
channelId
string
#5 joinGuild

Joins the guilds room. Requires member access. Receives guild member & channel events.

Payload
string
guildId
string
#6 startTyping

Emits the username to the channel they are typing in.

Payload
string
channelId
string
username
string
#7 stopTyping

Removes the username from the channel they were typing in.

Payload
string
channelId
string
username
string
#8 getRequestCount

Gets the amount of friend requests the user has.

#9 leaveGuild

Leaves the guild room.

Payload
string
guildId
string
#10 leaveRoom

Leaves the room.

Payload
string
roomId
string
#11 addChannel

A channel was created.

Payload
object

see ChannelResponse

id
string
name
string
isPublic
boolean
createdAt
string
updatedAt
string
hasNotification
boolean

Additional properties are allowed.

#12 deleteChannel

A channel was deleted.

Payload
string
id
string
#13 editChannel

A channel was edited

Payload
object

see ChannelResponse

id
string
name
string
isPublic
boolean
createdAt
string
updatedAt
string
hasNotification
boolean

Additional properties are allowed.

#14 editGuild

A guild was edited

Payload
object

see Guild

name
string
icon
string

Additional properties are allowed.

#15 deleteGuild

A guild was deleted.

Payload
string
id
string
#16 addMember

A member got added to the guild.

Payload
object

see MemberResponse

id
string
username
string
image
string
isOnline
string
createdAt
string
updatedAt
string
isFriend
string

Additional properties are allowed.

#17 removeMember

A member was removed from the guild.

Payload
string
id
string
#18 new_message

A new message was sent to a channel.

Payload
object
user
object

see MemberResponse

Additional properties are allowed.

id
string
text
string
url
string
filetype
string
createdAt
string
updatedAt
string

Additional properties are allowed.

#19 edit_message

A message in this channel was edited.

Payload
object
user
object

see MemberResponse

Additional properties are allowed.

id
string
text
string
url
string
filetype
string
createdAt
string
updatedAt
string

Additional properties are allowed.

#20 delete_message

A message in this channel was deleted.

Payload
string
id
string
#21 push_to_top

A notification that pushes the DM to the top of the list.

Payload
string
dmChannelId
string
#22 new_notification

A new message notification, published to all guild members. Additionally sends the channelId to members that currently view the guild.

Payload
string
channelId
string
guildId
string
#23 toggle_online

A notification that the user went online. Gets emited to guild members that currently view the guild and friends of the user.

Payload
string
userId
string
#24 toggle_offline

A notification that the user went offline. Gets emited to guild members that currently view the guild and friends of the user.

Payload
string
userId
string
#25 addToTyping

Emits the username to the channel the user is currently typing in.

Payload
string
username
string
#26 removeFromTyping

Emits the username to the channel the user was typing in.

Payload
string
username
string
#27 send_request

Emits a notification that a friends request was received

#28 add_friend

Adds the added person to the friends list.

Payload
object

see MemberResponse

member
object

Additional properties are allowed.

Additional properties are allowed.

#29 remove_friend

Removes the former friend from the friends list.

Payload
string
id
string
#30 requestCount

The amount of friends requests the user has

Payload
number
count
number
================================================ FILE: server/static/js/main.js ================================================ /* eslint-disable */ function bindExpanders() { var props = document.querySelectorAll('.js-prop'); for (let index = 0; index < props.length; index++) { const prop = props[index]; prop.addEventListener('click', function (ev) { ev.stopPropagation(); ev.currentTarget.parentElement.classList.toggle('is-open'); }); } } function highlightCode() { var blocks = document.querySelectorAll('.hljs code'); for (var i = 0; i < blocks.length; i++) { hljs.highlightBlock(blocks[i]); } } function bindMenuItems() { var items = document.querySelectorAll('.js-menu-item'); for (var i = 0; i < items.length; i++) { items[i].addEventListener('click', function () { document.getElementById("burger-menu").checked = false; }); } } window.addEventListener('load', highlightCode); window.addEventListener('load', bindExpanders); window.addEventListener('load', bindMenuItems); ================================================ FILE: server/ws/actions.go ================================================ package ws // Subscribed Messages const ( JoinUserAction = "joinUser" JoinGuildAction = "joinGuild" JoinChannelAction = "joinChannel" JoinVoiceAction = "joinVoice" LeaveGuildAction = "leaveGuild" LeaveRoomAction = "leaveRoom" LeaveVoiceAction = "leaveVoice" StartTypingAction = "startTyping" StopTypingAction = "stopTyping" ToggleOnlineAction = "toggleOnline" ToggleOfflineAction = "toggleOffline" GetRequestCountAction = "getRequestCount" ) // Emitted Messages const ( NewMessageAction = "new_message" EditMessageAction = "edit_message" DeleteMessageAction = "delete_message" AddChannelAction = "add_channel" AddPrivateChannelAction = "add_private_channel" EditChannelAction = "edit_channel" DeleteChannelAction = "delete_channel" EditGuildAction = "edit_guild" DeleteGuildAction = "delete_guild" RemoveFromGuildAction = "remove_from_guild" AddMemberAction = "add_member" RemoveMemberAction = "remove_member" NewDMNotificationAction = "new_dm_notification" NewNotificationAction = "new_notification" ToggleOnlineEmission = "toggle_online" ToggleOfflineEmission = "toggle_offline" AddToTypingAction = "addToTyping" RemoveFromTypingAction = "removeFromTyping" SendRequestAction = "send_request" AddRequestAction = "add_request" AddFriendAction = "add_friend" RemoveFriendAction = "remove_friend" PushToTopAction = "push_to_top" RequestCountEmission = "requestCount" VoiceSignal = "voice-signal" ToggleMute = "toggle-mute" ToggleDeafen = "toggle-deafen" ) ================================================ FILE: server/ws/client.go ================================================ package ws import ( "encoding/json" "github.com/gin-gonic/gin" "github.com/sentrionic/valkyrie/model" "log" "net/http" "time" "github.com/gorilla/websocket" ) const ( // Max wait time when writing message to peer writeWait = 10 * time.Second // Max time till next pong from peer pongWait = 60 * time.Second // Send ping interval, must be less then pong wait time pingPeriod = (pongWait * 9) / 10 // Maximum message size allowed from peer. maxMessageSize = 10000 ) var newline = []byte{'\n'} var upgrader = websocket.Upgrader{ ReadBufferSize: 4096, WriteBufferSize: 4096, CheckOrigin: func(r *http.Request) bool { return true }, } // Client represents the websockets client at the server type Client struct { // The actual websockets connection. ID string conn *websocket.Conn hub *Hub send chan []byte rooms map[*Room]bool } func newClient(conn *websocket.Conn, hub *Hub, id string) *Client { return &Client{ ID: id, conn: conn, hub: hub, send: make(chan []byte, 256), rooms: make(map[*Room]bool), } } func (client *Client) readPump() { defer func() { client.disconnect() }() client.conn.SetReadLimit(maxMessageSize) _ = client.conn.SetReadDeadline(time.Now().Add(pongWait)) client.conn.SetPongHandler(func(string) error { _ = client.conn.SetReadDeadline(time.Now().Add(pongWait)) return nil }) // Start endless read loop, waiting for messages from client for { _, jsonMessage, err := client.conn.ReadMessage() if err != nil { break } client.handleNewMessage(jsonMessage) } } func (client *Client) writePump() { ticker := time.NewTicker(pingPeriod) defer func() { ticker.Stop() _ = client.conn.Close() }() for { select { case message, ok := <-client.send: _ = client.conn.SetWriteDeadline(time.Now().Add(writeWait)) if !ok { // The hub closed the channel. _ = client.conn.WriteMessage(websocket.CloseMessage, []byte{}) return } w, err := client.conn.NextWriter(websocket.TextMessage) if err != nil { return } _, _ = w.Write(message) // Attach queued chat messages to the current websockets message. n := len(client.send) for i := 0; i < n; i++ { _, _ = w.Write(newline) _, _ = w.Write(<-client.send) } if err := w.Close(); err != nil { return } case <-ticker.C: _ = client.conn.SetWriteDeadline(time.Now().Add(writeWait)) if err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil { return } } } } func (client *Client) disconnect() { client.hub.unregister <- client for room := range client.rooms { room.unregister <- client } close(client.send) _ = client.conn.Close() } // ServeWs handles websockets requests from clients requests. func ServeWs(hub *Hub, ctx *gin.Context) { userId := ctx.MustGet("userId").(string) conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) if err != nil { log.Println(err) return } client := newClient(conn, hub, userId) go client.writePump() go client.readPump() hub.register <- client } func (client *Client) handleNewMessage(jsonMessage []byte) { var message model.ReceivedMessage if err := json.Unmarshal(jsonMessage, &message); err != nil { log.Printf("Error on unmarshal JSON message %s", err) } switch message.Action { // Join Room Actions case JoinChannelAction: client.handleJoinChannelMessage(message) case JoinGuildAction: client.handleJoinGuildMessage(message) case JoinUserAction: client.handleJoinRoomMessage(message) case JoinVoiceAction: client.handleJoinVoiceMessage(message) // Leave Room Actions case LeaveRoomAction: client.handleLeaveRoomMessage(message) case LeaveGuildAction: client.handleLeaveGuildMessage(message) case LeaveVoiceAction: client.handleLeaveVoiceMessage(message) // Chat Typing Actions case StartTypingAction: client.handleTypingEvent(message, AddToTypingAction) case StopTypingAction: client.handleTypingEvent(message, RemoveFromTypingAction) // Online Status Actions case ToggleOnlineAction: client.toggleOnlineStatus(true) case ToggleOfflineAction: client.toggleOnlineStatus(false) // Other case GetRequestCountAction: client.handleGetRequestCount() // Voice Chat case VoiceSignal: client.handleVoiceSignal(message) case ToggleMute: fallthrough case ToggleDeafen: client.updateVCMember(message) } } // handleJoinChannelMessage joins the given room if the user is a member in it func (client *Client) handleJoinChannelMessage(message model.ReceivedMessage) { roomName := message.Room cs := client.hub.channelService channel, err := cs.Get(roomName) if err != nil { return } // Check if the user has access to the given channel if err = cs.IsChannelMember(channel, client.ID); err != nil { return } client.handleJoinRoomMessage(message) } // handleJoinGuildMessage joins the given guild if the user is member in it func (client *Client) handleJoinGuildMessage(message model.ReceivedMessage) { roomName := message.Room gs := client.hub.guildService guild, err := gs.GetGuild(roomName) if err != nil { return } // Check if the user is member of the given guild if !isMember(guild, client.ID) { return } client.handleJoinRoomMessage(message) } // handleJoinRoomMessage joins the given room func (client *Client) handleJoinRoomMessage(message model.ReceivedMessage) { roomName := message.Room room := client.hub.findRoomById(roomName) if room == nil { room = client.hub.createRoom(roomName) } client.rooms[room] = true room.register <- client } // handleLeaveGuildMessage leaves the room and updates the members last seen date func (client *Client) handleLeaveGuildMessage(message model.ReceivedMessage) { _ = client.hub.guildService.UpdateMemberLastSeen(client.ID, message.Room) client.handleLeaveRoomMessage(message) } // handleLeaveRoomMessage leaves the room func (client *Client) handleLeaveRoomMessage(message model.ReceivedMessage) { room := client.hub.findRoomById(message.Room) delete(client.rooms, room) if room != nil { room.unregister <- client } } // handleGetRequestCount returns the users incoming friend request count func (client *Client) handleGetRequestCount() { if room := client.hub.findRoomById(client.ID); room != nil { count, err := client.hub.userService.GetRequestCount(client.ID) if err != nil { return } msg := model.WebsocketMessage{ Action: RequestCountEmission, Data: count, } room.broadcast <- &msg } } // handleTypingEvent emits the username of the currently typing user to the room func (client *Client) handleTypingEvent(message model.ReceivedMessage, action string) { roomID := message.Room if room := client.hub.findRoomById(roomID); room != nil { msg := model.WebsocketMessage{ Action: action, Data: message.Message, } room.broadcast <- &msg } } // toggleOnlineStatus updates the users online status and emits it to all // guilds the user is a member of and all of their friends func (client *Client) toggleOnlineStatus(isOnline bool) { uid := client.ID us := client.hub.userService user, err := us.Get(uid) if err != nil { log.Printf("could not find user: %v", err) return } user.IsOnline = isOnline if err := us.UpdateAccount(user); err != nil { log.Printf("could not update user: %v", err) return } ids, err := us.GetFriendAndGuildIds(uid) if err != nil { log.Printf("could not find ids: %v", err) return } action := ToggleOfflineEmission if isOnline { action = ToggleOnlineEmission } for _, id := range *ids { if room := client.hub.findRoomById(id); room != nil { msg := model.WebsocketMessage{ Action: action, Data: uid, } room.broadcast <- &msg } } } // handleJoinGuildMessage joins the given guild's voice chat if the user is a member in it func (client *Client) handleJoinVoiceMessage(message model.ReceivedMessage) { roomName := message.Room room := client.hub.findRoomById(roomName) if room == nil { room = client.hub.createRoom(roomName) } client.rooms[room] = true room.register <- client uid := client.ID us := client.hub.userService user, err := us.Get(uid) if err != nil { log.Printf("could not find user: %v", err) return } guild, err := client.hub.guildService.GetGuild(room.GetId()) if err != nil { log.Printf("could not find guild: %v", err) return } if !isMember(guild, user.ID) { return } guild.VCMembers = append(guild.VCMembers, *user) _ = client.hub.guildService.UpdateGuild(guild) clients, err := client.hub.guildService.GetVCMembers(guild.ID) if err != nil { log.Printf("could not get vc members: %v", err) return } msg := model.WebsocketMessage{ Action: message.Action, Data: gin.H{ "userId": user.ID, "clients": clients, }, } room.broadcast <- &msg } // handleVoiceSignal exchanges the messages needed to setup WebRTC func (client *Client) handleVoiceSignal(message model.ReceivedMessage) { data := (*message.Message).(map[string]any) receiver := data["userId"] if receiver == "" { return } data["userId"] = client.ID if room := client.hub.findRoomById(message.Room); room != nil { for c := range room.clients { if c.ID == receiver { msg := model.WebsocketMessage{ Action: message.Action, Data: data, } room.broadcast <- &msg break } } } } // handleLeaveVoiceMessage leaves the voice chat and the room func (client *Client) handleLeaveVoiceMessage(message model.ReceivedMessage) { if room := client.hub.findRoomById(message.Room); room != nil { _ = client.hub.guildService.RemoveVCMember(client.ID, message.Room) client.handleLeaveRoomMessage(message) guild, err := client.hub.guildService.GetGuild(room.GetId()) if err != nil { log.Printf("could not find guild: %v", err) return } clients, err := client.hub.guildService.GetVCMembers(guild.ID) if err != nil { log.Printf("could not get vc members: %v", err) return } msg := model.WebsocketMessage{ Action: message.Action, Data: gin.H{ "userId": client.ID, "clients": clients, }, } room.broadcast <- &msg } } // updateVCMember updates the values of the user in the voice chat func (client *Client) updateVCMember(message model.ReceivedMessage) { data := (*message.Message).(map[string]any) value := data["value"].(bool) user, err := client.hub.guildService.GetVCMember(client.ID, message.Room) if err != nil { log.Printf("could not find vc member: %v", err) return } if message.Action == ToggleMute { user.IsMuted = value } else if message.Action == ToggleDeafen { user.IsDeafened = value } err = client.hub.guildService.UpdateVCMember(user.IsMuted, user.IsDeafened, client.ID, message.Room) if err != nil { log.Printf("could not update vc member: %v", err) return } if room := client.hub.findRoomById(message.Room); room != nil { msg := model.WebsocketMessage{ Action: message.Action, Data: message.Message, } room.broadcast <- &msg } } // isMember checks if the user is member of the given guild func isMember(guild *model.Guild, userId string) bool { for _, v := range guild.Members { if v.ID == userId { return true } } return false } ================================================ FILE: server/ws/hub.go ================================================ package ws import ( "github.com/redis/go-redis/v9" "github.com/sentrionic/valkyrie/model" ) // Hub contains all rooms and clients type Hub struct { clients map[*Client]bool register chan *Client unregister chan *Client broadcast chan []byte rooms map[*Room]bool channelService model.ChannelService guildService model.GuildService userService model.UserService redisClient *redis.Client } // Config will hold services that will eventually be injected into this // service layer type Config struct { UserService model.UserService GuildService model.GuildService ChannelService model.ChannelService Redis *redis.Client } // NewWebsocketHub creates a new Hub func NewWebsocketHub(c *Config) *Hub { return &Hub{ clients: make(map[*Client]bool), register: make(chan *Client), unregister: make(chan *Client), broadcast: make(chan []byte), rooms: make(map[*Room]bool), channelService: c.ChannelService, guildService: c.GuildService, userService: c.UserService, redisClient: c.Redis, } } // Run our websocket server, accepting various requests func (hub *Hub) Run() { for { select { case client := <-hub.register: hub.registerClient(client) case client := <-hub.unregister: hub.unregisterClient(client) case message := <-hub.broadcast: hub.broadcastToClients(message) } } } func (hub *Hub) registerClient(client *Client) { hub.clients[client] = true } func (hub *Hub) unregisterClient(client *Client) { delete(hub.clients, client) } func (hub *Hub) broadcastToClients(message []byte) { for client := range hub.clients { client.send <- message } } // BroadcastToRoom sends the given message to all clients connected to the given room func (hub *Hub) BroadcastToRoom(message []byte, roomId string) { if room := hub.findRoomById(roomId); room != nil { room.publishRoomMessage(message) } } func (hub *Hub) findRoomById(id string) *Room { var foundRoom *Room for room := range hub.rooms { if room.GetId() == id { foundRoom = room break } } return foundRoom } func (hub *Hub) createRoom(id string) *Room { room := NewRoom(id, hub.redisClient) go room.RunRoom() hub.rooms[room] = true return room } ================================================ FILE: server/ws/room.go ================================================ package ws import ( "context" "github.com/redis/go-redis/v9" "github.com/sentrionic/valkyrie/model" "log" ) // Room represents a websocket room type Room struct { id string clients map[*Client]bool register chan *Client unregister chan *Client broadcast chan *model.WebsocketMessage redis *redis.Client } var ctx = context.Background() // NewRoom creates a new Room func NewRoom(id string, rds *redis.Client) *Room { return &Room{ id: id, clients: make(map[*Client]bool), register: make(chan *Client), unregister: make(chan *Client), broadcast: make(chan *model.WebsocketMessage), redis: rds, } } // RunRoom runs our room, accepting various requests func (room *Room) RunRoom() { go room.subscribeToRoomMessages() for { select { case client := <-room.register: room.registerClientInRoom(client) case client := <-room.unregister: room.unregisterClientInRoom(client) case message := <-room.broadcast: room.publishRoomMessage(message.Encode()) } } } // registerClientInRoom adds the client to the room func (room *Room) registerClientInRoom(client *Client) { room.clients[client] = true } // unregisterClientInRoom removes the client from the room func (room *Room) unregisterClientInRoom(client *Client) { delete(room.clients, client) } // broadcastToClientsInRoom sends the given message to all members in the room func (room *Room) broadcastToClientsInRoom(message []byte) { for client := range room.clients { client.send <- message } } // GetId returns the ID of the room func (room *Room) GetId() string { return room.id } // publishRoomMessage publishes the message to all clients subscribing to the room func (room *Room) publishRoomMessage(message []byte) { err := room.redis.Publish(ctx, room.GetId(), message).Err() if err != nil { log.Println(err) } } // subscribeToRoomMessages subscribes to messages in this room func (room *Room) subscribeToRoomMessages() { pubsub := room.redis.Subscribe(ctx, room.GetId()) ch := pubsub.Channel() for msg := range ch { room.broadcastToClientsInRoom([]byte(msg.Payload)) } } ================================================ FILE: web/.eslintrc.json ================================================ { "env": { "browser": true, "es2021": true }, "extends": ["plugin:react/recommended", "airbnb", "airbnb-typescript", "prettier"], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { "jsx": true }, "ecmaVersion": 12, "sourceType": "module", "project": "./tsconfig.json" }, "plugins": ["react", "@typescript-eslint", "react-hooks", "prettier"], "rules": { "linebreak-style": 0, "no-undef": "off", "no-empty": "off", "no-use-before-define": "off", "@typescript-eslint/no-use-before-define": ["off"], "react/jsx-filename-extension": [ "warn", { "extensions": [".tsx"] } ], "import/extensions": [ "error", "ignorePackages", { "ts": "never", "tsx": "never" } ], "@typescript-eslint/explicit-function-return-type": [ "error", { "allowExpressions": true } ], "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "error", // or error { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" } ], "react/jsx-no-useless-fragment": "off", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "react/require-default-props": "off", "react/prop-types": "off", "import/prefer-default-export": "off", "max-len": [ "error", { "code": 120 } ], "react/function-component-definition": [ 2, { "namedComponents": "arrow-function", "unnamedComponents": "arrow-function" } ] }, "settings": { "import/resolver": { "typescript": {} } }, "ignorePatterns": ["*.css", "*.html"] } ================================================ FILE: web/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.development .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* .eslintcache /cypress/downloads ================================================ FILE: web/.prettierrc ================================================ { "printWidth": 120, "semi": true, "tabWidth": 2, "singleQuote": true } ================================================ FILE: web/cypress/e2e/account.cy.ts ================================================ import { uuid } from '../support/utils'; // Covers the account routes describe('Account related pages', () => { const id = uuid(); const email = `${id}@example.com`; it('redirects an unauthenticated user', () => { cy.visit('/channels/me'); cy.url().should('include', '/login'); }); it('registers the user', () => { cy.registerUser(email, id); }); it('signs in the user', () => { cy.loginUser(email); }); it("checks the user's settings page", () => { cy.loginUser(email); cy.get('[aria-label=settings]').click(); // Confirm account page got user's info cy.url().should('include', '/account'); cy.contains('My Account'.toUpperCase()); cy.get('input[name="email"]').should('have.value', email); cy.get('input[name="username"]').should('have.value', id); }); it("updates the user's info", () => { cy.loginUser(email); cy.get('[aria-label=settings]').click(); // Update the user's info and check for confirmation toast cy.url().should('include', '/account'); cy.contains('My Account'.toUpperCase()); cy.get('input[name="email"]').should('have.value', email); cy.get('input[name="username"]').clear().type('Test').should('have.value', 'Test'); cy.get('[type=submit]').click(); cy.wait(200); cy.contains('Account Updated.'); }); it("updates the user's password", () => { cy.loginUser(email); cy.get('[aria-label=settings]').click(); cy.url().should('include', '/account'); cy.contains('Change Password').click(); // Change the user's password cy.contains('Change your password'); cy.get('input[name="currentPassword"]').type('password').should('have.value', 'password'); cy.get('input[name="newPassword"]').type('password').should('have.value', 'password'); cy.get('input[name="confirmNewPassword"]').type('password').should('have.value', 'password'); cy.contains('Done').click(); cy.wait(200); cy.contains('Changed Password'); }); it('signs out the user', () => { cy.loginUser(email); cy.get('[aria-label=settings]').click(); cy.url().should('include', '/account'); cy.contains('Logout').click(); cy.url().should('include', ''); }); }); ================================================ FILE: web/cypress/e2e/channel.cy.ts ================================================ import { uuid } from '../support/utils'; describe('Channels related actions', () => { // The user's values const id = uuid().toString(); const email = `${id}@example.com`; let guildId = ''; let channelId = ''; it('registers the user', () => { cy.registerUser(email, id); }); it('creates a guild', () => { cy.loginUser(email); cy.intercept({ method: 'POST', pathname: '/api/guilds/create', }).as('create'); cy.createGuild(id); // Confirm the user got sent to the guild and default channel cy.wait('@create').then((interception) => { const body = interception.response.body; const url = `channels/${body.id}/${body.default_channel_id}`; cy.url().should('include', url); guildId = body.id; }); }); it('creates a channel for the guild', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.openGuildMenu(); cy.intercept({ method: 'POST', pathname: `/api/channels/${guildId}`, }).as('create'); cy.contains('Create Channel').click(); cy.get('input[name="name"]').type('random').should('have.value', 'random'); cy.get('[type=submit]').click(); // Confirm the user got sent to the newly created channel cy.wait('@create').then((interception) => { const body = interception.response.body; channelId = body.id; const url = `channels/${guildId}/${channelId}`; cy.url().should('include', url); cy.contains('random').should('exist'); }); }); it('should successfully switch between channels', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.contains('Welcome to #general').should('exist'); // Check the other channel is the correct one cy.get(`a[href^="/channels/${guildId}/"]`).last().click(); cy.contains('Welcome to #random').should('exist'); }); it('should successfully edit the channel', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.contains('random').trigger('mouseover'); cy.get('[aria-label="edit channel"]').click(); cy.intercept({ method: 'PUT', pathname: `/api/channels/${channelId}`, }).as('update'); // Edit the values cy.get('input[name="name"]').clear().type('secret').should('have.value', 'secret'); cy.get('input[type="checkbox"]').check({ force: true }); cy.get('[type=submit]').click(); // Check that the edited channel exists cy.wait('@update').then((_) => { cy.contains('secret').should('exist'); }); }); it('should successfully delete the channel', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.get(`a[href^="/channels/${guildId}/"]`).last().click(); cy.contains('secret').last().trigger('mouseover'); cy.get('[aria-label="edit channel"]').last().click(); cy.intercept({ method: 'DELETE', pathname: `/api/channels/${channelId}`, }).as('delete'); cy.contains('Delete Channel').click(); cy.get('button').contains('Delete Channel').click(); cy.wait('@delete').then((interception) => { // Check that the channel is gone and that the user got moved cy.contains('secret').should('have.length.lte', 1); cy.contains('Welcome to #general').should('exist'); }); }); }); ================================================ FILE: web/cypress/e2e/friend.cy.ts ================================================ import { uuid } from '../support/utils'; describe('Friends related actions', () => { // Friend values const friendName = uuid(); const friendEmail = `${friendName}@example.com`; let friendId = ''; // AuthUser values const authName = uuid(); const email = `${authName}@example.com`; it('registers the friend and gets the id', () => { cy.registerUser(friendEmail, friendName); // Get the friend's ID and store it cy.contains('Add Friend').click(); cy.get('input') .first() .invoke('val') .then((text) => { friendId = text.toString(); }); }); it('signs up the user and gets the id', () => { cy.registerUser(email, authName); }); it('sends a friend request to the member', () => { cy.loginUser(email); cy.sendRequest(friendId); // Confirm the 'Pending' tab got an outgoing friend request cy.contains('Pending').click(); cy.contains(friendName); cy.contains('Outgoing Friend Request'); }); it('cancels the outgoing request', () => { cy.loginUser(email); cy.contains('Pending').click(); cy.contains(friendName); // Confirm the accept button does not exist cy.get('[aria-label="accept request"]').should('not.exist'); cy.get('[aria-label="decline request"]').click(); // Confirm the user got removed and is not a friend. cy.contains(friendName).should('not.exist'); cy.get('button').contains('Friends').click(); cy.contains(friendName).should('not.exist'); }); it('sends another friend request to the member to be checked', () => { cy.loginUser(email); cy.sendRequest(friendId); }); it('checks the incoming friend request and declines it', () => { cy.loginUser(friendEmail); // Confirm the user got a request and has the accept button cy.contains('Pending').click(); cy.contains(authName); cy.contains('Incoming Friend Request'); cy.get('[aria-label="accept request"]').should('exist'); // Decline request cy.get('[aria-label="decline request"]').click(); cy.contains(authName).should('not.exist'); // Confirm the user got removed and is not a friend. cy.get('button').contains('Friends').click(); cy.wait(100); cy.contains(authName).should('not.exist'); }); it('sends another friend request to the member to be accepted', () => { cy.loginUser(email); cy.sendRequest(friendId); }); it('accepts the friend request', () => { cy.loginUser(friendEmail); // Accept request cy.contains('Pending').click(); cy.contains(authName); cy.get('[aria-label="accept request"]').click(); cy.contains(authName).should('not.exist'); // Confirm the user is in the 'Friends' tab cy.get('button').contains('Friends').click(); cy.wait(100); cy.contains(authName).should('exist'); }); it('directs to the friends dms when clicked', () => { cy.loginUser(email); cy.intercept({ method: 'POST', pathname: `/api/channels/${friendId}/dm`, }).as('getDM'); // Open the DM cy.contains(friendName).should('exist').click(); // Confirm it's the friend's DM cy.contains(friendName).should('exist'); cy.contains(`This is the beginning of your direct message history with @${friendName}`).should('exist'); cy.get('textarea[name="text"]').invoke('attr', 'placeholder').should('contain', `@${friendName}`); // Confirm the DM's url is the correct one cy.wait('@getDM').then((interception) => { const body = interception.response.body; const url = `/channels/me/${body.id}`; cy.url().should('include', url); }); }); it('should successfully go to the DM when clicked on the item', () => { cy.loginUser(email); cy.get('ul[id="dm-list"]').children().contains(friendName).click(); // Confirm it's the friend's DM cy.contains(friendName).should('exist'); cy.contains(`This is the beginning of your direct message history with @${friendName}`).should('exist'); cy.get('textarea[name="text"]').invoke('attr', 'placeholder').should('contain', `@${friendName}`); // Check that messaging is possible and the message gets added to the chat cy.get('textarea[name="text"]').type('Hello World{enter}').should('have.value', ''); cy.wait(50); cy.contains('Hello World'); }); it('closes the dm when the close button is pressed', () => { cy.loginUser(email); // Check that the DM exists cy.get('ul[id="dm-list"]').children().contains(friendName).should('exist'); cy.get('ul[id="dm-list"] li:first').trigger('mouseover'); // Close the DM and confirm it's gone cy.get('[aria-label="close dm"]').click(); cy.get('ul[id="dm-list"]').children().contains(friendName).should('not.exist'); }); it('removes the friend', () => { cy.loginUser(email); // Check that the friend exists in the tab cy.get('ul[id="friend-list"]').children().contains(friendName).should('exist'); // Confirm and remove friend cy.get('[aria-label="remove friend"]').click(); cy.contains('Remove Friend').click(); // Confirm the friend got removed cy.get('ul[id="friend-list"]').should('not.exist'); }); }); ================================================ FILE: web/cypress/e2e/guild.cy.ts ================================================ import { uuid } from '../support/utils'; describe('Guild related actions', () => { // The user's values const id = uuid().toString(); const email = `${id}@example.com`; // The mock members values const memberId = uuid().toString(); const memberEmail = `${memberId}@example.com`; // The invite link and guildId let invite = ''; let guildId = ''; it('registers the user', () => { cy.registerUser(email, id); }); it('creates a guild', () => { cy.loginUser(email); cy.intercept({ method: 'POST', pathname: '/api/guilds/create', }).as('create'); cy.createGuild(id); // Confirm the user got sent to the guild and default channel cy.wait('@create').then((interception) => { const body = interception.response.body; const url = `channels/${body.id}/${body.default_channel_id}`; cy.url().should('include', url); guildId = body.id; }); }); it('should update the server after it got edited', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.openGuildMenu(); cy.contains('Server Settings').click(); cy.intercept({ method: 'PUT', pathname: `/api/guilds/${guildId}`, }).as('update'); // Updates the values and saves them cy.get('input[name="name"]').clear().type('Valkyrie').should('have.value', 'Valkyrie'); cy.contains('Save Changes').click(); // Check the updates were applied cy.wait('@update').then((_) => { cy.contains('Valkyrie').should('exist'); }); }); it('should successfully clear the invites', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.openGuildMenu(); cy.contains('Server Settings').click(); cy.intercept({ method: 'DELETE', pathname: `/api/guilds/${guildId}/invite`, }).as('clear'); cy.contains('Invalidate Links').click(); // Check the invites succesfully got cleared cy.wait('@clear').then((interception) => { const body = interception.response.body; expect(body).eq(true); }); }); it('should delete the server and go to home', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Delete server and confirm the request cy.openGuildMenu(); cy.contains('Server Settings').click(); cy.contains('Delete Server').click(); cy.contains('Delete Server').click(); // Confirm the user is back at home and the guild is gone // Not working due to websocket connection problems // cy.url().should('include', '/channels/me'); // cy.get('a[href*="channels"]').not('a[href*="channels/me"]').should('have.length', 0); }); it('creates another guild to get an invite', () => { cy.loginUser(email); cy.intercept({ method: 'POST', pathname: '/api/guilds/create', }).as('create'); cy.createGuild(id); // Confirm the user got sent to the guild and default channel cy.wait('@create').then((interception) => { const body = interception.response.body; const url = `channels/${body.id}/${body.default_channel_id}`; cy.url().should('include', url); guildId = body.id; }); // get the invite for the other user cy.openGuildMenu(); cy.contains('Invite People').click(); // Check unlimited invites cy.get('input[type="checkbox"]').check({ force: true }); cy.wait(50); // Store the invite in the variable cy.get('input[id="invite-link"]') .first() .invoke('val') .then((text) => { invite = text.toString(); }); }); it('joins the guild for the given link', () => { cy.registerUser(memberEmail, memberId); cy.intercept({ method: 'POST', pathname: '/api/guilds/join', }).as('join'); cy.joinGuild(invite); // Confirm the user got sent to the guild and default channel cy.wait('@join').then((interception) => { const body = interception.response.body; const url = `channels/${body.id}/${body.default_channel_id}`; cy.url().should('include', url); }); cy.contains(`${id}'s server`).should('exist'); cy.contains(`Welcome to #general`).should('exist'); }); it('should not show "Server Settings" and "Create Channel" to the non owner', () => { cy.loginUser(memberEmail); cy.clickOnFirstGuild(); cy.openGuildMenu(); cy.contains('Server Settings').should('not.exist'); cy.contains('Create Channel').should('not.exist'); }); it('should leave the server', () => { cy.loginUser(memberEmail); cy.clickOnFirstGuild(); cy.openGuildMenu(); cy.contains('Leave Server').click(); // Confirm the user is back at home and the guild is gone cy.url().should('include', '/channels/me'); cy.get('a[href*="channels"]').not('a[href*="channels/me"]').should('have.length', 0); }); it('creates a third guild to test switching', () => { cy.loginUser(email); cy.createGuild('Test'); }); it('successfully switches between the guilds', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.url().should('include', guildId); cy.contains(`${id}'s server`).should('exist'); // Click on the second server and confirm it's the second one created cy.get('a[href*="channels"]').eq(2).click(); cy.contains(`Test's server`).should('exist'); }); }); ================================================ FILE: web/cypress/e2e/member.cy.ts ================================================ import { uuid } from '../support/utils'; describe('Members related actions', () => { // Member values const memberName = uuid(); const memberEmail = `${memberName}@example.com`; let invite = ''; // AuthUser values const authName = uuid(); const email = `${authName}@example.com`; it('registers the user', () => { cy.registerUser(email, authName); }); it('creates a guild and get the invite', () => { cy.loginUser(email); cy.createGuild(authName); // get the invite for the other user cy.openGuildMenu(); cy.contains('Invite People').click(); // Check unlimited invites cy.get('input[type="checkbox"]').check({ force: true }); cy.wait(50); // Store the invite in the variable cy.get('input[id="invite-link"]') .first() .invoke('val') .then((text) => { invite = text.toString(); }); }); it('successfully changes the members settings', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.openGuildMenu(); cy.contains('Change Appearance').click(); // Sets the member values cy.get('input[name="nickname"]').clear().type('Tester').should('have.value', 'Tester'); cy.get('div[title="#0693E3"]').click(); cy.wait(100); cy.contains('Save').click(); }); it('joins the guild for the given link', () => { cy.registerUser(memberEmail, memberName); cy.joinGuild(invite); // Confirm the above changes cy.wait(100); cy.contains('Tester').should('exist'); // Check that the two members + two labels are there cy.get('ul[id="member-list"]').children().should('have.length', 4); }); it('should not show "Kick" and "Ban" options to the non owner', () => { cy.loginUser(memberEmail); cy.clickOnFirstGuild(); cy.get('ul[id="member-list"]').contains('Tester').rightclick(); cy.contains('Ban').should('not.exist'); cy.contains('Kick').should('not.exist'); }); it('successfully resets the members settings', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.openGuildMenu(); cy.contains('Change Appearance').click(); // Reset the values cy.contains('Reset Nickname').click(); cy.contains('Reset Color').click(); cy.wait(100); cy.contains('Save').click(); }); it('should go to the members DMs on click', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.contains(memberName).rightclick(); cy.contains('Message').click(); // Confirm the user got moved to the correct DM cy.contains(memberName).should('exist'); cy.contains(`This is the beginning of your direct message history with @${memberName}`).should('exist'); cy.get('textarea[name="text"]').invoke('attr', 'placeholder').should('contain', `@${memberName}`); }); it('should successfully sent a friends request', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.contains(memberName).rightclick(); cy.contains('Add Friend').click(); // Go to the pending tab to confirm the request cy.get('a[href="/channels/me"]').click(); cy.contains('Pending').click(); cy.contains(memberName).should('exist'); cy.contains('Outgoing Friend Request').should('exist'); }); it('should kick and remove the member', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Kick the user cy.contains(memberName).rightclick(); cy.contains(`Kick ${memberName}`).click(); cy.get('button').contains('Kick').click(); // Confirm the member is gone cy.get('ul[id="member-list"]').children().should('have.length', 3); cy.contains(memberName).should('not.exist'); }); it('should successfully rejoin the server', () => { cy.loginUser(memberEmail); cy.joinGuild(invite); }); it('should ban and remove the member', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Ban the user cy.contains(memberName).rightclick(); cy.contains(`Ban ${memberName}`).click(); cy.get('button').contains('Ban').click(); // Confirm the member is gone cy.get('ul[id="member-list"]').children().should('have.length', 3); cy.contains(memberName).should('not.exist'); }); it('should not be able to rejoin the server', () => { cy.loginUser(memberEmail); cy.joinGuild(invite); // Confirm the user did not join the guild cy.contains('You are banned from this server').should('exist'); cy.url().should('include', '/channels/me'); }); it('should contain the member in the ban list', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Go to the ban list modal cy.openGuildMenu(); cy.contains('Server Settings').click(); cy.contains('Bans').click(); // Check the member exists there cy.wait(100); cy.contains(memberName).should('exist'); cy.get('button[aria-label="unban user"]').click(); // Confirm the member got unbanned cy.contains(memberName).should('not.exist'); }); it('should not display a context menu when clicking on oneself', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.contains(authName).rightclick(); cy.contains('Add Friend').should('not.exist'); }); it('should toggle the member list on click', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Toggle the member list cy.get('ul[id="member-list"]').should('be.visible'); cy.get('[aria-label="toggle member list"]').click(); cy.get('ul[id="member-list"]').should('not.exist'); cy.get('[aria-label="toggle member list"]').click(); cy.get('ul[id="member-list"]').should('be.visible'); }); }); ================================================ FILE: web/cypress/e2e/message.cy.ts ================================================ import { uuid } from '../support/utils'; const waitTime = 1000; describe('Message related actions', () => { // The user's values const id = uuid().toString(); const email = `${id}@example.com`; // The mock members values const memberName = uuid().toString(); const memberEmail = `${memberName}@example.com`; let userId = ''; // The invite link and guildId let invite = ''; it('should register the user and create a guild', () => { cy.intercept({ method: 'POST', pathname: '/api/account/register', }).as('register'); cy.registerUser(email, id); cy.wait('@register').then((interception) => { const body = interception.response.body; userId = body.id; }); cy.createGuild(id); }); it('should successfully post a message', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Send message cy.get('textarea[name="text"]').type('Hello, World{enter}').should('have.value', ''); // Confirm it got added to the chat // cy.wait(100); // cy.getChat().should('exist'); // cy.getChat().children().should('have.length', 1); // cy.firstMessage().contains('Hello, World'); }); it('should confirm that the message got sent', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.wait(waitTime); cy.getChat().should('exist'); cy.getChat().children().should('have.length', 1); cy.firstMessage().contains('Hello, World'); }); it('should post another message', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.get('textarea[name="text"]').type('Hello, Server{enter}').should('have.value', ''); }); it('should display newer messages under older', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Confirm it got added under the first one cy.wait(waitTime); cy.getChat().children().should('have.length', 2); cy.firstMessage().contains('Hello, Server'); cy.getChat().children().last().contains('Hello, World'); }); it('should successfully delete the message', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.firstMessage().rightclick(); cy.contains('Delete Message').click(); cy.get('button').contains('Delete').click(); }); // Manually check the message is gone because of websocket problems it('should confirm that the message got deleted', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.wait(waitTime); cy.getChat().children().should('have.length', 1); cy.getChat().children().contains('Hello, Server').should('not.exist'); }); it('should successfully edit the message', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.firstMessage().rightclick(); cy.contains('Edit Message').click(); cy.wait(waitTime); cy.get('input[id="editMessage"]').clear().type('Hello, Update'); cy.get('button').contains('Save').click(); // // Confirm it got edited and got the edit span // cy.firstMessage().contains('Hello, Update'); // cy.firstMessage().contains('(edited)'); }); it('should confirm the message got edited', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Confirm it got edited and got the edit span cy.firstMessage().contains('Hello, Update'); cy.firstMessage().contains('(edited)'); }); it('should not display "Add Friend" or "Message" for own avatar', () => { cy.loginUser(email); cy.clickOnFirstGuild(); cy.wait(waitTime); cy.firstMessage().get('img').eq(1).rightclick(); cy.contains('Add Friend').should('not.exist'); cy.contains('Message').should('not.exist'); }); it('should get an invite for the other member', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // get the invite for the other user cy.openGuildMenu(); cy.contains('Invite People').click(); // Check unlimited invites cy.get('input[type="checkbox"]').check({ force: true }); cy.wait(waitTime); // Store the invite in the variable cy.get('input[id="invite-link"]') .first() .invoke('val') .then((text) => { invite = text.toString(); }); }); it('should register the other member and join the guild', () => { cy.registerUser(memberEmail, memberName); cy.joinGuild(invite); }); it('should not display message options for the member context menu when clicking on the owners message', () => { cy.loginUser(memberEmail); cy.clickOnFirstGuild(); cy.getChat().children().last().rightclick(); cy.contains('Edit Message').should('not.exist'); cy.contains('Delete Message').should('not.exist'); }); it('should successfully add the user', () => { cy.loginUser(memberEmail); cy.clickOnFirstGuild(); cy.intercept({ method: 'POST', pathname: `/api/account/${userId}/friend`, }).as('addFriend'); cy.wait(waitTime); cy.firstMessage().get('img').eq(1).rightclick(); cy.contains('Add Friend').click(); cy.wait('@addFriend').then((_) => { // Go to the pending tab to confirm the request cy.get('a[href="/channels/me"]').click(); cy.contains('Pending').click(); cy.contains(memberName).should('exist'); cy.contains('Outgoing Friend Request').should('exist'); }); }); it('should successfully go to the DMs with the user', () => { cy.loginUser(memberEmail); cy.clickOnFirstGuild(); cy.intercept({ method: 'POST', pathname: `/api/channels/${userId}/dm`, }).as('create'); cy.wait(waitTime); cy.firstMessage().get('img').eq(1).rightclick(); cy.contains('Message').click(); cy.wait('@create').then(() => { // Confirm the user got moved to the correct DM cy.contains(id).should('exist'); cy.contains(`This is the beginning of your direct message history with @${id}`).should('exist'); cy.get('textarea[name="text"]').invoke('attr', 'placeholder').should('contain', `@${id}`); }); }); it('should successfully post a message', () => { cy.loginUser(memberEmail); cy.clickOnFirstGuild(); cy.get('textarea[name="text"]').type('Hello, Owner{enter}').should('have.value', ''); }); it("should be able to delete the member's message as the owner", () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Owner can delete the other person's message, but cannot edit it cy.firstMessage().rightclick(); cy.contains('Edit Message').should('not.exist'); cy.contains('Delete Message').click(); cy.get('button').contains('Delete').click(); }); it('should confirm that the message got deleted', () => { cy.loginUser(email); cy.clickOnFirstGuild(); // Confirm the message got deleted cy.wait(100); cy.getChat().children().should('have.length', 1); cy.getChat().children().contains('Hello, Owner').should('not.exist'); }); }); ================================================ FILE: web/cypress/fixtures/example.json ================================================ { "name": "Using fixtures to represent data", "email": "hello@cypress.io", "body": "Fixtures are a great way to mock data for responses to routes" } ================================================ FILE: web/cypress/plugins/index.js ================================================ /// // *********************************************************** // This example plugins/index.js can be used to load plugins // // You can change the location of this file or turn off loading // the plugins file with the 'pluginsFile' configuration option. // // You can read more here: // https://on.cypress.io/plugins-guide // *********************************************************** // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) /** * @type {Cypress.PluginConfig} */ // eslint-disable-next-line no-unused-vars module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config }; ================================================ FILE: web/cypress/support/commands.ts ================================================ Cypress.Commands.add('registerUser', (email, id) => { cy.visit('/'); cy.contains('Valkyrie'); cy.contains('Register').click(); cy.url().should('include', '/register'); cy.get('input[name="email"]').type(email).should('have.value', email); cy.get('input[name="username"]').type(id).should('have.value', id); cy.get('input[name="password"]').type('password').should('have.value', 'password'); cy.contains('Register').click(); cy.url().should('include', '/channels/me'); }); Cypress.Commands.add('loginUser', (email) => { cy.visit('/'); cy.contains('Valkyrie'); cy.contains('Login').click(); cy.url().should('include', '/login'); cy.get('input[name="email"]').type(email).should('have.value', email); cy.get('input[name="password"]').type('password').should('have.value', 'password'); cy.contains('Login').click(); cy.url().should('include', '/channels/me'); }); Cypress.Commands.add('sendRequest', (id) => { cy.contains('Add Friend').click(); cy.get('input[name=id]').type(id).should('have.value', id); cy.get('[type=submit]').click(); }); Cypress.Commands.add('createGuild', (id) => { cy.get('[id="add-guild-icon"]').click(); cy.contains('Create My Own').click(); cy.get('input').clear().type(`${id}'s server`); cy.get('[type=submit]').click(); cy.contains(`${id}'s server`).should('exist'); cy.contains(`Welcome to #general`).should('exist'); }); Cypress.Commands.add('clickOnFirstGuild', () => { cy.wait(1000); cy.get('a[href*="channels"]').not('a[href*="channels/me"]').first().click(); }); Cypress.Commands.add('openGuildMenu', () => { cy.get('[id^="menu-button-"]').click(); }); Cypress.Commands.add('joinGuild', (invite) => { cy.get('[id="add-guild-icon"]').click(); cy.contains('Join a Server').click(); cy.get('input').type(invite).should('have.value', invite); cy.get('[type=submit]').click(); cy.wait(1000); }); Cypress.Commands.add('getChat', () => { cy.get('.infinite-scroll-component'); }); Cypress.Commands.add('firstMessage', () => { cy.getChat().children().first(); }); ================================================ FILE: web/cypress/support/e2e.js ================================================ // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. // // This is a great place to put global configuration and // behavior that modifies Cypress. // // You can change the location of this file or turn off // automatically serving support files with the // 'supportFile' configuration option. // // You can read more here: // https://on.cypress.io/configuration // *********************************************************** // Import commands.js using ES2015 syntax: import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') ================================================ FILE: web/cypress/support/index.d.ts ================================================ // load type definitions that come with Cypress module // eslint-disable-next-line spaced-comment /// declare namespace Cypress { interface Chainable { registerUser(email: string, id: string): Chainable; loginUser(email: string): Chainable; sendRequest(id: string): Chainable; createGuild(id: string): Chainable; joinGuild(invite: string): Chainable; clickOnFirstGuild(): Chainable; openGuildMenu(): Chainable; getChat(): Chainable; firstMessage(): Chainable; } } ================================================ FILE: web/cypress/support/utils.ts ================================================ export const uuid = (): string => Cypress._.random(0, 1e6).toString(); ================================================ FILE: web/cypress/tsconfig.json ================================================ { "compilerOptions": { "target": "es5", "lib": ["es5", "dom"], "types": ["cypress"] }, "include": ["**/*.ts", "support/index.js"] } ================================================ FILE: web/cypress.config.ts ================================================ import { defineConfig } from 'cypress'; export default defineConfig({ video: false, screenshotOnRunFailure: false, retries: 2, e2e: { baseUrl: 'http://localhost:3000', }, defaultCommandTimeout: 8000, }); ================================================ FILE: web/package.json ================================================ { "name": "valkyrie", "version": "0.1.0", "private": true, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "lint": "npx eslint src/**", "prettier": "npx prettier --write **/*.{ts,js,css,html,tsx}", "compile": "tsc", "workflow": "yarn compile && yarn prettier && yarn lint && yarn test --watchAll=false", "cypress": "npx cypress open" }, "dependencies": { "@chakra-ui/react": "^2.7.1", "@chakra-ui/theme-tools": "^2.0.18", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@tanstack/react-query": "^4.29.15", "@testing-library/jest-dom": "^5.16.5", "axios": "^1.4.0", "chakra-ui-autocomplete": "^1.4.5", "dayjs": "^1.11.8", "formik": "^2.4.2", "framer-motion": "^10.12.17", "msw": "^1.2.2", "react": "^18.2.0", "react-color": "^2.19.3", "react-contexify": "^5.0.0", "react-dom": "^18.2.0", "react-easy-crop": "^4.7.5", "react-icons": "4.10.1", "react-infinite-scroll-component": "^6.1.0", "react-router-dom": "^6.14.0", "react-scripts": "5.0.1", "react-textarea-autosize": "^8.5.0", "reconnecting-websocket": "^4.4.0", "yup": "^1.2.0", "zustand": "^4.3.8" }, "devDependencies": { "@testing-library/dom": "^9.3.1", "@testing-library/react": "^14.0.0", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.4.3", "@types/node": "^20.3.1", "@types/react": "^18.2.14", "@types/react-color": "^3.0.6", "@types/react-dom": "^18.2.6", "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^5.60.0", "@typescript-eslint/parser": "^5.60.0", "cypress": "12.15.0", "eslint": "^8.43.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.0.0", "eslint-config-prettier": "^8.8.0", "eslint-import-resolver-typescript": "^3.5.5", "eslint-plugin-import": "^2.27.5", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^2.8.8", "react-test-renderer": "^18.2.0", "typescript": "^5.1.3" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "jest": { "transform": { "^.+\\.[t|j]sx?$": "babel-jest" }, "transformIgnorePatterns": [ "node_modules/(?!axios)/" ] } } ================================================ FILE: web/public/_redirects ================================================ /* /index.html 200 ================================================ FILE: web/public/index.html ================================================ Valkyrie
================================================ FILE: web/public/manifest.json ================================================ { "short_name": "React App", "name": "Create React App Sample", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: web/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: web/src/App.tsx ================================================ import * as React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { AppRoutes } from './routes/Routes'; import { GlobalState } from './components/sections/GlobalState'; const client = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, staleTime: Infinity, cacheTime: 0, }, }, }); export const App: React.FC = () => ( ); ================================================ FILE: web/src/components/common/GuildPills.tsx ================================================ import { Box } from '@chakra-ui/react'; import React from 'react'; export const NotificationIndicator: React.FC = () => ( ); export const ChannelNotificationIndicator: React.FC = () => ( ); export const ActiveGuildPill: React.FC = () => ( ); export const HoverGuildPill: React.FC = () => ( ); ================================================ FILE: web/src/components/common/InputField.tsx ================================================ /* eslint-disable react/jsx-props-no-spreading */ import React, { InputHTMLAttributes } from 'react'; import { useField } from 'formik'; import { FormControl, FormErrorMessage, FormLabel, Input, Text } from '@chakra-ui/react'; type InputFieldProps = InputHTMLAttributes & { label: string; name: string; }; export const InputField: React.FC = ({ label, ...props }) => { const [field, { error, touched }] = useField(props); return ( {label} {/* @ts-ignore */} {error} ); }; ================================================ FILE: web/src/components/common/Logo.tsx ================================================ import { Box, Image, Text } from '@chakra-ui/react'; import React from 'react'; export const Logo: React.FC = () => ( ); ================================================ FILE: web/src/components/common/NotificationIcon.tsx ================================================ import { Flex, Text } from '@chakra-ui/react'; import React from 'react'; interface NotificationIconProps { count: number; } export const NotificationIcon: React.FC = ({ count }) => ( {count} ); export const PingIcon: React.FC = ({ count }) => ( {count} ); ================================================ FILE: web/src/components/items/ChannelListItem.tsx ================================================ import React, { useEffect, useState } from 'react'; import { Flex, Icon, ListItem, Text, useDisclosure } from '@chakra-ui/react'; import { FaHashtag, FaUserLock } from 'react-icons/fa'; import { MdSettings } from 'react-icons/md'; import { Link, useLocation } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { userStore } from '../../lib/stores/userStore'; import { ChannelSettingsModal } from '../modals/ChannelSettingsModal'; import { useGetCurrentGuild } from '../../lib/utils/hooks/useGetCurrentGuild'; import { ChannelNotificationIndicator } from '../common/GuildPills'; import { cKey } from '../../lib/utils/querykeys'; import { Channel } from '../../lib/models/channel'; interface ChannelListItemProps { channel: Channel; guildId: string; } export const ChannelListItem: React.FC = ({ channel, guildId }) => { const currentPath = `/channels/${guildId}/${channel.id}`; const location = useLocation(); const isActive = location.pathname === currentPath; const [showSettings, setShowSettings] = useState(false); const current = userStore((state) => state.current); const guild = useGetCurrentGuild(guildId); const { isOpen, onOpen, onClose } = useDisclosure(); const cache = useQueryClient(); useEffect(() => { if (channel.hasNotification && isActive) { cache.setQueryData([cKey, guildId], (d) => { if (!d) return []; return d.map((c) => (c.id === channel.id ? { ...c, hasNotification: false } : c)); }); } }); return ( setShowSettings(false)} onMouseEnter={() => setShowSettings(true)} > {channel.hasNotification && } {channel.name} {current?.id === guild?.ownerId && (showSettings || isOpen) && ( <> { e.preventDefault(); onOpen(); }} /> {isOpen && ( )} )} ); }; ================================================ FILE: web/src/components/items/DMListItem.tsx ================================================ import React, { useState } from 'react'; import { Avatar, AvatarBadge, Flex, Icon, ListItem, Text } from '@chakra-ui/react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { IoMdClose } from 'react-icons/io'; import { useQueryClient } from '@tanstack/react-query'; import { closeDirectMessage } from '../../lib/api/handler/dm'; import { dmKey } from '../../lib/utils/querykeys'; import { DMChannel } from '../../lib/models/dm'; interface DMListItemProps { dm: DMChannel; } export const DMListItem: React.FC = ({ dm }) => { const currentPath = `/channels/me/${dm.id}`; const location = useLocation(); const isActive = location.pathname === currentPath; const [showCloseButton, setShowButton] = useState(false); const navigate = useNavigate(); const cache = useQueryClient(); const closeDM = async (): Promise => { try { await closeDirectMessage(dm.id); cache.setQueryData([dmKey], (d) => d?.filter((c) => c.id !== dm.id) ?? []); if (isActive) { navigate('/channels/me', { replace: true }); } } catch (err) {} }; return ( setShowButton(false)} onMouseEnter={() => setShowButton(true)} > {dm.user.username} {showCloseButton && ( { e.preventDefault(); await closeDM(); }} /> )} ); }; ================================================ FILE: web/src/components/items/FriendsListItem.tsx ================================================ import { Avatar, AvatarBadge, Flex, IconButton, ListItem, Text, useDisclosure } from '@chakra-ui/react'; import React from 'react'; import { FaEllipsisV } from 'react-icons/fa'; import { useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { getOrCreateDirectMessage } from '../../lib/api/handler/dm'; import { RemoveFriendModal } from '../modals/RemoveFriendModal'; import { dmKey } from '../../lib/utils/querykeys'; import { Friend } from '../../lib/models/friend'; import { DMChannel } from '../../lib/models/dm'; interface FriendsListItemProp { friend: Friend; } export const FriendsListItem: React.FC = ({ friend }) => { const navigate = useNavigate(); const { isOpen, onOpen, onClose } = useDisclosure(); const cache = useQueryClient(); const getDMChannel = async (): Promise => { try { const { data } = await getOrCreateDirectMessage(friend.id); if (data) { cache.setQueryData([dmKey], (d) => { const queryData = d ?? []; const index = queryData.findIndex((dm) => dm.id === data.id); if (index === -1) return [data, ...queryData]; return queryData; }); navigate(`/channels/me/${data.id}`); } } catch (err) {} }; return ( {friend.username} } borderRadius="50%" aria-label="remove friend" onClick={(e) => { e.preventDefault(); onOpen(); }} /> {isOpen && } ); }; ================================================ FILE: web/src/components/items/GuildListItem.tsx ================================================ import React, { useEffect, useState } from 'react'; import { Avatar, Flex } from '@chakra-ui/react'; import { Link, useLocation } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { StyledTooltip } from '../sections/StyledTooltip'; import { ActiveGuildPill, HoverGuildPill, NotificationIndicator } from '../common/GuildPills'; import { gKey } from '../../lib/utils/querykeys'; import { Guild } from '../../lib/models/guild'; interface GuildListItemProps { guild: Guild; } export const GuildListItem: React.FC = ({ guild }) => { const location = useLocation(); const isActive = location.pathname.includes(guild.id); const [isHover, setHover] = useState(false); const cache = useQueryClient(); useEffect(() => { if (guild.hasNotification && isActive) { cache.setQueryData([gKey], (data) => { if (!data) return []; return data.map((g) => (g.id === guild.id ? { ...g, hasNotification: false } : g)); }); } }); return ( {isActive && } {isHover && } {guild.hasNotification && } {guild.icon ? ( setHover(false)} onMouseEnter={() => setHover(true)} /> ) : ( setHover(false)} onMouseEnter={() => setHover(true)} > {guild.name[0]} )} ); }; ================================================ FILE: web/src/components/items/MemberListItem.tsx ================================================ import React from 'react'; import { Avatar, AvatarBadge, Flex, ListItem, Text } from '@chakra-ui/react'; import { useContextMenu } from 'react-contexify'; import { useParams } from 'react-router-dom'; import { useGetCurrentGuild } from '../../lib/utils/hooks/useGetCurrentGuild'; import { userStore } from '../../lib/stores/userStore'; import { MemberContextMenu } from '../menus/MemberContextMenu'; import { RouterProps } from '../../lib/models/routerProps'; import { Member } from '../../lib/models/member'; interface MemberListItemProps { member: Member; } export const MemberListItem: React.FC = ({ member }) => { const current = userStore((state) => state.current); const { guildId } = useParams() as RouterProps; const guild = useGetCurrentGuild(guildId); const isOwner = guild !== undefined && guild.ownerId === current?.id; const { show } = useContextMenu({ id: member.id, }); return ( <> {member.nickname ?? member.username} {member.id !== current?.id && } ); }; ================================================ FILE: web/src/components/items/NotificationListItem.tsx ================================================ import React, { useEffect, useState } from 'react'; import { Avatar, Flex } from '@chakra-ui/react'; import { Link, useLocation } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { StyledTooltip } from '../sections/StyledTooltip'; import { ActiveGuildPill, HoverGuildPill, NotificationIndicator } from '../common/GuildPills'; import { NotificationIcon } from '../common/NotificationIcon'; import { dmKey, nKey } from '../../lib/utils/querykeys'; import { DMChannel, DMNotification } from '../../lib/models/dm'; interface NotificationListItemProps { notification: DMNotification; } export const NotificationListItem: React.FC = ({ notification }) => { const location = useLocation(); const isActive = location.pathname.includes(notification.id); const [isHover, setHover] = useState(false); const cache = useQueryClient(); useEffect(() => { if (isActive) { cache.setQueryData([nKey], (d) => d?.filter((c) => c.id !== notification.id) ?? []); } }); const handleClick = (): void => { if (window.location.pathname.includes('/channels/me')) { const newChannel: DMChannel = { id: notification.id, user: notification.user, }; cache.setQueryData([dmKey], (d) => { const data = d ?? []; const index = d!.findIndex((dm) => dm.id === notification.id); if (index === -1) return [newChannel, ...data]; return data; }); } }; return ( {isActive && } {isHover && } setHover(false)} onMouseEnter={() => setHover(true)} onClick={() => handleClick()} > ); }; ================================================ FILE: web/src/components/items/RequestListItem.tsx ================================================ import { Avatar, Box, Flex, IconButton, ListItem, Text } from '@chakra-ui/react'; import React from 'react'; import { BiCheck } from 'react-icons/bi'; import { AiOutlineClose } from 'react-icons/ai'; import { useQueryClient } from '@tanstack/react-query'; import { StyledTooltip } from '../sections/StyledTooltip'; import { acceptFriendRequest, declineFriendRequest } from '../../lib/api/handler/account'; import { fKey, rKey } from '../../lib/utils/querykeys'; import { FriendRequest, RequestType } from '../../lib/models/friend'; interface RequestListItemProps { request: FriendRequest; } export const RequestListItem: React.FC = ({ request }) => { const cache = useQueryClient(); const acceptRequest = async (): Promise => { try { const { data } = await acceptFriendRequest(request.id); if (data) { cache.setQueryData([rKey], (d) => d?.filter((r) => r.id !== request.id) ?? []); await cache.invalidateQueries([fKey]); } } catch (err) {} }; const declineRequest = async (): Promise => { try { const { data } = await declineFriendRequest(request.id); if (data) { cache.setQueryData([rKey], (d) => d?.filter((r) => r.id !== request.id) ?? []); } } catch (err) {} }; return ( {request.username} {request.type === RequestType.INCOMING ? 'Incoming Friend Request' : 'Outgoing Friend Request'} {request.type === 1 && ( } borderRadius="50%" aria-label="accept request" fontSize="28px" onClick={acceptRequest} mr="2" /> )} } borderRadius="50%" aria-label="decline request" fontSize="20px" onClick={declineRequest} /> ); }; ================================================ FILE: web/src/components/items/VoiceChannelItem.tsx ================================================ import { Avatar, Flex, Icon, Text } from '@chakra-ui/react'; import React, { useCallback } from 'react'; import { MdHeadsetOff, MdMicOff } from 'react-icons/md'; interface VoiceUserVisualProps { username: string; image: string; isMuted: boolean; isDeafened: boolean; } const VoiceUserVisual: React.FC = ({ username, image, isMuted, isDeafened }) => ( {username} {isDeafened && } {isMuted && } ); interface VoiceUserProps extends VoiceUserVisualProps { stream?: MediaStream | null; muted?: boolean; controls?: boolean; } export const VoiceChannelItem: React.FC = ({ username, image, stream, muted = false, controls = false, isMuted, isDeafened, }) => { const refAudio = useCallback( (node: HTMLAudioElement) => { if (node && stream) { const audio = node; audio.srcObject = stream; } }, [stream] ); if (!stream) return ; return ( <> {/* eslint-disable-next-line jsx-a11y/media-has-caption */}