diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 8dea6c2..0000000 --- a/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,3 +0,0 @@ -wrapperVersion=3.3.4 -distributionType=only-script -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/README.md b/README.md index e69de29..82ec4ef 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,90 @@ +# Система управления банковскими картами + +REST API на Spring Boot для управления банковскими картами с JWT авторизацией, шифрованием номеров карт и ролевым доступом. + +## Быстрый старт + +### Запуск через Docker Compose + +```bash +docker-compose up -d +``` + +Приложение будет доступно на `http://localhost:8080`. + +### Запуск вручную + +1. Запустите PostgreSQL: +```bash +docker run -d --name postgres \ + -e POSTGRES_DB=bankcards \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -p 5432:5432 postgres:15 +``` + +2. Соберите и запустите приложение: +```bash +mvn clean package -DskipTests +java -jar target/bankcards-0.0.1-SNAPSHOT.jar +``` + +## API документация + +После запуска Swagger UI доступен по адресу: +`http://localhost:8080/swagger-ui.html` + +## Аутентификация + +Все запросы (кроме `/api/auth/**`) требуют JWT токен в заголовке: +``` +Authorization: Bearer +``` + +**Дефолтный администратор:** +- Username: `admin` +- Password: `admin123` + +## Роли + +| Роль | Возможности | +|------|-------------| +| `ROLE_ADMIN` | Создание/блокировка/активация/удаление карт; управление пользователями; просмотр всех карт | +| `ROLE_USER` | Просмотр своих карт; запрос блокировки; переводы между своими картами | + +## Основные эндпоинты + +### Аутентификация +- `POST /api/auth/register` — регистрация +- `POST /api/auth/login` — вход + +### Карты (пользователь) +- `GET /api/cards` — мои карты (фильтр по статусу, пагинация) +- `GET /api/cards/{id}` — карта по id +- `POST /api/cards/{id}/request-block` — запросить блокировку +- `POST /api/cards/transfer` — перевод между своими картами + +### Карты (администратор) +- `POST /api/admin/cards` — создать карту +- `GET /api/admin/cards` — все карты +- `PATCH /api/admin/cards/{id}/block` — заблокировать +- `PATCH /api/admin/cards/{id}/activate` — активировать +- `DELETE /api/admin/cards/{id}` — удалить + +### Пользователи (администратор) +- `GET /api/admin/users` — все пользователи +- `PATCH /api/admin/users/{id}/role` — изменить роль +- `DELETE /api/admin/users/{id}` — удалить + +## Безопасность + +- Номера карт хранятся в зашифрованном виде (AES) +- Отображаются только в маскированном виде: `**** **** **** 1234` +- Пароли хешируются через BCrypt +- Доступ контролируется по ролям через Spring Security + +## Тесты + +```bash +mvn test +``` diff --git a/docs/openapi.yaml b/docs/openapi.yaml new file mode 100644 index 0000000..3bbf65d --- /dev/null +++ b/docs/openapi.yaml @@ -0,0 +1,300 @@ +openapi: 3.0.3 +info: + title: Bank Card Management API + description: REST API for managing bank cards with JWT authentication + version: 1.0.0 + +servers: + - url: http://localhost:8080 + description: Local development server + +security: + - BearerAuth: [] + +components: + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + RegisterRequest: + type: object + required: [username, email, password] + properties: + username: { type: string, minLength: 3, maxLength: 50 } + email: { type: string, format: email } + password: { type: string, minLength: 6 } + + LoginRequest: + type: object + required: [username, password] + properties: + username: { type: string } + password: { type: string } + + AuthResponse: + type: object + properties: + token: { type: string } + username: { type: string } + role: { type: string } + + CardResponse: + type: object + properties: + id: { type: integer } + maskedNumber: { type: string, example: "**** **** **** 1234" } + ownerUsername: { type: string } + expiryDate: { type: string, format: date } + status: { type: string, enum: [ACTIVE, BLOCKED, EXPIRED] } + balance: { type: number } + + CreateCardRequest: + type: object + required: [cardNumber, ownerId, expiryDate, initialBalance] + properties: + cardNumber: { type: string, pattern: '^\d{16}$' } + ownerId: { type: integer } + expiryDate: { type: string, format: date } + initialBalance: { type: number, minimum: 0 } + + TransferRequest: + type: object + required: [fromCardId, toCardId, amount] + properties: + fromCardId: { type: integer } + toCardId: { type: integer } + amount: { type: number, minimum: 0.01 } + + UserResponse: + type: object + properties: + id: { type: integer } + username: { type: string } + email: { type: string } + role: { type: string, enum: [ROLE_USER, ROLE_ADMIN] } + + ErrorResponse: + type: object + properties: + status: { type: integer } + message: { type: string } + timestamp: { type: string, format: date-time } + +paths: + /api/auth/register: + post: + tags: [Authentication] + summary: Register a new user + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/RegisterRequest" } + responses: + "200": + description: Registered successfully + content: + application/json: + schema: { $ref: "#/components/schemas/AuthResponse" } + "400": + description: Validation error or duplicate username/email + + /api/auth/login: + post: + tags: [Authentication] + summary: Login and get JWT token + security: [] + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/LoginRequest" } + responses: + "200": + description: Login successful + content: + application/json: + schema: { $ref: "#/components/schemas/AuthResponse" } + "401": + description: Invalid credentials + + /api/cards: + get: + tags: [Cards - User] + summary: Get own cards (with optional status filter and pagination) + parameters: + - name: status + in: query + schema: { type: string, enum: [ACTIVE, BLOCKED, EXPIRED] } + - name: page + in: query + schema: { type: integer, default: 0 } + - name: size + in: query + schema: { type: integer, default: 10 } + responses: + "200": + description: Paginated list of user's cards + + /api/cards/{id}: + get: + tags: [Cards - User] + summary: Get a specific card (must be owner) + parameters: + - name: id + in: path + required: true + schema: { type: integer } + responses: + "200": + description: Card details + content: + application/json: + schema: { $ref: "#/components/schemas/CardResponse" } + "403": + description: Access denied + "404": + description: Card not found + + /api/cards/{id}/request-block: + post: + tags: [Cards - User] + summary: Request to block own card + parameters: + - name: id + in: path + required: true + schema: { type: integer } + responses: + "200": + description: Block requested + + /api/cards/transfer: + post: + tags: [Cards - User] + summary: Transfer between own cards + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/TransferRequest" } + responses: + "200": + description: Transfer successful + "400": + description: Insufficient balance or inactive card + + /api/admin/cards: + get: + tags: [Cards - Admin] + summary: Get all cards + responses: + "200": + description: All cards paginated + post: + tags: [Cards - Admin] + summary: Create a new card + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/CreateCardRequest" } + responses: + "201": + description: Card created + + /api/admin/cards/{id}/block: + patch: + tags: [Cards - Admin] + summary: Block a card + parameters: + - name: id + in: path + required: true + schema: { type: integer } + responses: + "200": + description: Card blocked + + /api/admin/cards/{id}/activate: + patch: + tags: [Cards - Admin] + summary: Activate a card + parameters: + - name: id + in: path + required: true + schema: { type: integer } + responses: + "200": + description: Card activated + + /api/admin/cards/{id}: + delete: + tags: [Cards - Admin] + summary: Delete a card + parameters: + - name: id + in: path + required: true + schema: { type: integer } + responses: + "204": + description: Deleted + + /api/admin/users: + get: + tags: [Users - Admin] + summary: Get all users + responses: + "200": + description: Paginated list of users + + /api/admin/users/{id}: + get: + tags: [Users - Admin] + summary: Get user by ID + parameters: + - name: id + in: path + required: true + schema: { type: integer } + responses: + "200": + description: User details + delete: + tags: [Users - Admin] + summary: Delete user + parameters: + - name: id + in: path + required: true + schema: { type: integer } + responses: + "204": + description: Deleted + + /api/admin/users/{id}/role: + patch: + tags: [Users - Admin] + summary: Update user role + parameters: + - name: id + in: path + required: true + schema: { type: integer } + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + role: { type: string, enum: [ROLE_USER, ROLE_ADMIN] } + responses: + "200": + description: Role updated diff --git a/mvnw b/mvnw deleted file mode 100755 index bd8896b..0000000 --- a/mvnw +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/sh -# ---------------------------------------------------------------------------- -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# ---------------------------------------------------------------------------- - -# ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.3.4 -# -# Optional ENV vars -# ----------------- -# JAVA_HOME - location of a JDK home dir, required when download maven via java source -# MVNW_REPOURL - repo url base for downloading maven distribution -# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output -# ---------------------------------------------------------------------------- - -set -euf -[ "${MVNW_VERBOSE-}" != debug ] || set -x - -# OS specific support. -native_path() { printf %s\\n "$1"; } -case "$(uname)" in -CYGWIN* | MINGW*) - [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" - native_path() { cygpath --path --windows "$1"; } - ;; -esac - -# set JAVACMD and JAVACCMD -set_java_home() { - # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched - if [ -n "${JAVA_HOME-}" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - JAVACCMD="$JAVA_HOME/jre/sh/javac" - else - JAVACMD="$JAVA_HOME/bin/java" - JAVACCMD="$JAVA_HOME/bin/javac" - - if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then - echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 - echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 - return 1 - fi - fi - else - JAVACMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v java - )" || : - JAVACCMD="$( - 'set' +e - 'unset' -f command 2>/dev/null - 'command' -v javac - )" || : - - if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then - echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 - return 1 - fi - fi -} - -# hash string like Java String::hashCode -hash_string() { - str="${1:-}" h=0 - while [ -n "$str" ]; do - char="${str%"${str#?}"}" - h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) - str="${str#?}" - done - printf %x\\n $h -} - -verbose() { :; } -[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - -die() { - printf %s\\n "$1" >&2 - exit 1 -} - -trim() { - # MWRAPPER-139: - # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. - # Needed for removing poorly interpreted newline sequences when running in more - # exotic environments such as mingw bash on Windows. - printf "%s" "${1}" | tr -d '[:space:]' -} - -scriptDir="$(dirname "$0")" -scriptName="$(basename "$0")" - -# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties -while IFS="=" read -r key value; do - case "${key-}" in - distributionUrl) distributionUrl=$(trim "${value-}") ;; - distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; - esac -done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" -[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" - -case "${distributionUrl##*/}" in -maven-mvnd-*bin.*) - MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ - case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in - *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; - :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; - :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; - :Linux*x86_64*) distributionPlatform=linux-amd64 ;; - *) - echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 - distributionPlatform=linux-amd64 - ;; - esac - distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" - ;; -maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; -*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; -esac - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" -distributionUrlName="${distributionUrl##*/}" -distributionUrlNameMain="${distributionUrlName%.*}" -distributionUrlNameMain="${distributionUrlNameMain%-bin}" -MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" -MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - -exec_maven() { - unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : - exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" -} - -if [ -d "$MAVEN_HOME" ]; then - verbose "found existing MAVEN_HOME at $MAVEN_HOME" - exec_maven "$@" -fi - -case "${distributionUrl-}" in -*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; -*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; -esac - -# prepare tmp dir -if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then - clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } - trap clean HUP INT TERM EXIT -else - die "cannot create temp dir" -fi - -mkdir -p -- "${MAVEN_HOME%/*}" - -# Download and Install Apache Maven -verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -verbose "Downloading from: $distributionUrl" -verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -# select .zip or .tar.gz -if ! command -v unzip >/dev/null; then - distributionUrl="${distributionUrl%.zip}.tar.gz" - distributionUrlName="${distributionUrl##*/}" -fi - -# verbose opt -__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' -[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v - -# normalize http auth -case "${MVNW_PASSWORD:+has-password}" in -'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; -has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; -esac - -if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then - verbose "Found wget ... using wget" - wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" -elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then - verbose "Found curl ... using curl" - curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" -elif set_java_home; then - verbose "Falling back to use Java to download" - javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" - targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" - cat >"$javaSource" <<-END - public class Downloader extends java.net.Authenticator - { - protected java.net.PasswordAuthentication getPasswordAuthentication() - { - return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); - } - public static void main( String[] args ) throws Exception - { - setDefault( new Downloader() ); - java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); - } - } - END - # For Cygwin/MinGW, switch paths to Windows format before running javac and java - verbose " - Compiling Downloader.java ..." - "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" - verbose " - Running Downloader.java ..." - "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" -fi - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -if [ -n "${distributionSha256Sum-}" ]; then - distributionSha256Result=false - if [ "$MVN_CMD" = mvnd.sh ]; then - echo "Checksum validation is not supported for maven-mvnd." >&2 - echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - elif command -v sha256sum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then - distributionSha256Result=true - fi - elif command -v shasum >/dev/null; then - if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then - distributionSha256Result=true - fi - else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 - echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 - exit 1 - fi - if [ $distributionSha256Result = false ]; then - echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 - echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 - exit 1 - fi -fi - -# unzip and move -if command -v unzip >/dev/null; then - unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" -else - tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" -fi - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -actualDistributionDir="" - -# First try the expected directory name (for regular distributions) -if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then - if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then - actualDistributionDir="$distributionUrlNameMain" - fi -fi - -# If not found, search for any directory with the Maven executable (for snapshots) -if [ -z "$actualDistributionDir" ]; then - # enable globbing to iterate over items - set +f - for dir in "$TMP_DOWNLOAD_DIR"/*; do - if [ -d "$dir" ]; then - if [ -f "$dir/bin/$MVN_CMD" ]; then - actualDistributionDir="$(basename "$dir")" - break - fi - fi - done - set -f -fi - -if [ -z "$actualDistributionDir" ]; then - verbose "Contents of $TMP_DOWNLOAD_DIR:" - verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" - die "Could not find Maven distribution directory in extracted archive" -fi - -verbose "Found extracted Maven distribution directory: $actualDistributionDir" -printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" -mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" - -clean || : -exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd deleted file mode 100644 index 92450f9..0000000 --- a/mvnw.cmd +++ /dev/null @@ -1,189 +0,0 @@ -<# : batch portion -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.3.4 -@REM -@REM Optional ENV vars -@REM MVNW_REPOURL - repo url base for downloading maven distribution -@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven -@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output -@REM ---------------------------------------------------------------------------- - -@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) -@SET __MVNW_CMD__= -@SET __MVNW_ERROR__= -@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% -@SET PSModulePath= -@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( - IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) -) -@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% -@SET __MVNW_PSMODULEP_SAVE= -@SET __MVNW_ARG0_NAME__= -@SET MVNW_USERNAME= -@SET MVNW_PASSWORD= -@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) -@echo Cannot start maven from wrapper >&2 && exit /b 1 -@GOTO :EOF -: end batch / begin powershell #> - -$ErrorActionPreference = "Stop" -if ($env:MVNW_VERBOSE -eq "true") { - $VerbosePreference = "Continue" -} - -# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties -$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl -if (!$distributionUrl) { - Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" -} - -switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { - "maven-mvnd-*" { - $USE_MVND = $true - $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" - $MVN_CMD = "mvnd.cmd" - break - } - default { - $USE_MVND = $false - $MVN_CMD = $script -replace '^mvnw','mvn' - break - } -} - -# apply MVNW_REPOURL and calculate MAVEN_HOME -# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ -if ($env:MVNW_REPOURL) { - $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } - $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" -} -$distributionUrlName = $distributionUrl -replace '^.*/','' -$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' - -$MAVEN_M2_PATH = "$HOME/.m2" -if ($env:MAVEN_USER_HOME) { - $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" -} - -if (-not (Test-Path -Path $MAVEN_M2_PATH)) { - New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null -} - -$MAVEN_WRAPPER_DISTS = $null -if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { - $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" -} else { - $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" -} - -$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" -$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' -$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" - -if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { - Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" - Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" - exit $? -} - -if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { - Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" -} - -# prepare tmp dir -$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile -$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" -$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null -trap { - if ($TMP_DOWNLOAD_DIR.Exists) { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } - } -} - -New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null - -# Download and Install Apache Maven -Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." -Write-Verbose "Downloading from: $distributionUrl" -Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - -$webclient = New-Object System.Net.WebClient -if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { - $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) -} -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null - -# If specified, validate the SHA-256 sum of the Maven distribution zip file -$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum -if ($distributionSha256Sum) { - if ($USE_MVND) { - Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." - } - Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash - if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { - Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." - } -} - -# unzip and move -Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null - -# Find the actual extracted directory name (handles snapshots where filename != directory name) -$actualDistributionDir = "" - -# First try the expected directory name (for regular distributions) -$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" -$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" -if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { - $actualDistributionDir = $distributionUrlNameMain -} - -# If not found, search for any directory with the Maven executable (for snapshots) -if (!$actualDistributionDir) { - Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { - $testPath = Join-Path $_.FullName "bin/$MVN_CMD" - if (Test-Path -Path $testPath -PathType Leaf) { - $actualDistributionDir = $_.Name - } - } -} - -if (!$actualDistributionDir) { - Write-Error "Could not find Maven distribution directory in extracted archive" -} - -Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" -Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null -try { - Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null -} catch { - if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { - Write-Error "fail to move MAVEN_HOME" - } -} finally { - try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } - catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } -} - -Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 071ff17..8010405 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,7 @@ com.example bankcards 0.0.1-SNAPSHOT + bankcards Bank Card Management System diff --git a/src/main/java/com/example/bankcards/BankcardsApplication.java b/src/main/java/com/example/bankcards/BankCardsApplication.java similarity index 54% rename from src/main/java/com/example/bankcards/BankcardsApplication.java rename to src/main/java/com/example/bankcards/BankCardsApplication.java index a0d9e47..5ffaf71 100644 --- a/src/main/java/com/example/bankcards/BankcardsApplication.java +++ b/src/main/java/com/example/bankcards/BankCardsApplication.java @@ -4,10 +4,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class BankcardsApplication { - - public static void main(String[] args) { - SpringApplication.run(BankcardsApplication.class, args); - } +public class BankCardsApplication { + public static void main(String[] args) { + SpringApplication.run(BankCardsApplication.class, args); + } } diff --git a/src/main/java/com/example/bankcards/config/SecurityConfig.java b/src/main/java/com/example/bankcards/config/SecurityConfig.java index 95820c3..79d3d3b 100644 --- a/src/main/java/com/example/bankcards/config/SecurityConfig.java +++ b/src/main/java/com/example/bankcards/config/SecurityConfig.java @@ -4,6 +4,7 @@ import com.example.bankcards.security.JwtAuthenticationFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -43,6 +44,8 @@ public class SecurityConfig { "/swagger-ui.html" ) .permitAll() + .requestMatchers(HttpMethod.GET, "/api/admin/**") + .hasAuthority("ROLE_ADMIN") .requestMatchers("/api/admin/**") .hasAuthority("ROLE_ADMIN") .anyRequest() diff --git a/src/main/java/com/example/bankcards/controller/AdminCardController.java b/src/main/java/com/example/bankcards/controller/AdminCardController.java index b71fd4b..0573918 100644 --- a/src/main/java/com/example/bankcards/controller/AdminCardController.java +++ b/src/main/java/com/example/bankcards/controller/AdminCardController.java @@ -3,6 +3,9 @@ package com.example.bankcards.controller; import com.example.bankcards.dto.CardResponse; import com.example.bankcards.dto.CreateCardRequest; import com.example.bankcards.service.CardService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -16,11 +19,14 @@ import org.springframework.web.bind.annotation.*; @RequestMapping("/api/admin/cards") @RequiredArgsConstructor @PreAuthorize("hasAuthority('ROLE_ADMIN')") +@Tag(name = "Cards (Admin)", description = "Admin operations on cards") +@SecurityRequirement(name = "Bearer Authentication") public class AdminCardController { private final CardService cardService; @PostMapping + @Operation(summary = "Create a new card") public ResponseEntity createCard( @Valid @RequestBody CreateCardRequest request ) { @@ -30,26 +36,31 @@ public class AdminCardController { } @GetMapping + @Operation(summary = "Get all cards") public ResponseEntity> getAllCards(Pageable pageable) { return ResponseEntity.ok(cardService.getAllCards(pageable)); } @GetMapping("/{id}") + @Operation(summary = "Get card by id") public ResponseEntity getCard(@PathVariable Long id) { return ResponseEntity.ok(cardService.getCard(id, null, true)); } @PatchMapping("/{id}/block") + @Operation(summary = "Block a card") public ResponseEntity blockCard(@PathVariable Long id) { return ResponseEntity.ok(cardService.blockCard(id)); } @PatchMapping("/{id}/activate") + @Operation(summary = "Activate a card") public ResponseEntity activateCard(@PathVariable Long id) { return ResponseEntity.ok(cardService.activateCard(id)); } @DeleteMapping("/{id}") + @Operation(summary = "Delete a card") public ResponseEntity deleteCard(@PathVariable Long id) { cardService.deleteCard(id); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/example/bankcards/controller/AdminUserController.java b/src/main/java/com/example/bankcards/controller/AdminUserController.java index 317e909..cbab385 100644 --- a/src/main/java/com/example/bankcards/controller/AdminUserController.java +++ b/src/main/java/com/example/bankcards/controller/AdminUserController.java @@ -3,6 +3,9 @@ package com.example.bankcards.controller; import com.example.bankcards.dto.UpdateUserRoleRequest; import com.example.bankcards.dto.UserResponse; import com.example.bankcards.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -15,21 +18,26 @@ import org.springframework.web.bind.annotation.*; @RequestMapping("/api/admin/users") @RequiredArgsConstructor @PreAuthorize("hasAuthority('ROLE_ADMIN')") +@Tag(name = "Users (Admin)", description = "Admin operations on users") +@SecurityRequirement(name = "Bearer Authentication") public class AdminUserController { private final UserService userService; @GetMapping + @Operation(summary = "Get all users") public ResponseEntity> getAllUsers(Pageable pageable) { return ResponseEntity.ok(userService.getAllUsers(pageable)); } @GetMapping("/{id}") + @Operation(summary = "Get user by id") public ResponseEntity getUser(@PathVariable Long id) { return ResponseEntity.ok(userService.getUser(id)); } @PatchMapping("/{id}/role") + @Operation(summary = "Update user role") public ResponseEntity updateRole( @PathVariable Long id, @Valid @RequestBody UpdateUserRoleRequest request @@ -38,6 +46,7 @@ public class AdminUserController { } @DeleteMapping("/{id}") + @Operation(summary = "Delete user") public ResponseEntity deleteUser(@PathVariable Long id) { userService.deleteUser(id); return ResponseEntity.noContent().build(); diff --git a/src/main/java/com/example/bankcards/controller/AuthController.java b/src/main/java/com/example/bankcards/controller/AuthController.java index 045a336..739f9cc 100644 --- a/src/main/java/com/example/bankcards/controller/AuthController.java +++ b/src/main/java/com/example/bankcards/controller/AuthController.java @@ -4,6 +4,8 @@ import com.example.bankcards.dto.AuthResponse; import com.example.bankcards.dto.LoginRequest; import com.example.bankcards.dto.RegisterRequest; import com.example.bankcards.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -12,11 +14,13 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/auth") @RequiredArgsConstructor +@Tag(name = "Authentication", description = "Register and login") public class AuthController { private final AuthService authService; @PostMapping("/register") + @Operation(summary = "Register a new user") public ResponseEntity register( @Valid @RequestBody RegisterRequest request ) { @@ -24,6 +28,7 @@ public class AuthController { } @PostMapping("/login") + @Operation(summary = "Login and get JWT token") public ResponseEntity login( @Valid @RequestBody LoginRequest request ) { diff --git a/src/main/java/com/example/bankcards/controller/CardController.java b/src/main/java/com/example/bankcards/controller/CardController.java index dcb52a5..ababd93 100644 --- a/src/main/java/com/example/bankcards/controller/CardController.java +++ b/src/main/java/com/example/bankcards/controller/CardController.java @@ -4,6 +4,9 @@ import com.example.bankcards.dto.CardResponse; import com.example.bankcards.dto.TransferRequest; import com.example.bankcards.entity.CardStatus; import com.example.bankcards.service.CardService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -16,11 +19,14 @@ import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/cards") @RequiredArgsConstructor +@Tag(name = "Cards (User)", description = "User operations on their own cards") +@SecurityRequirement(name = "Bearer Authentication") public class CardController { private final CardService cardService; @GetMapping + @Operation(summary = "Get my cards with optional filter by status") public ResponseEntity> getMyCards( @AuthenticationPrincipal UserDetails userDetails, @RequestParam(required = false) CardStatus status, @@ -32,6 +38,7 @@ public class CardController { } @GetMapping("/{id}") + @Operation(summary = "Get a specific card (must own it)") public ResponseEntity getCard( @PathVariable Long id, @AuthenticationPrincipal UserDetails userDetails @@ -42,6 +49,7 @@ public class CardController { } @PostMapping("/{id}/request-block") + @Operation(summary = "Request to block own card") public ResponseEntity requestBlock( @PathVariable Long id, @AuthenticationPrincipal UserDetails userDetails @@ -51,6 +59,7 @@ public class CardController { } @PostMapping("/transfer") + @Operation(summary = "Transfer money between own cards") public ResponseEntity transfer( @Valid @RequestBody TransferRequest request, @AuthenticationPrincipal UserDetails userDetails diff --git a/src/main/java/com/example/bankcards/entity/CardStatus.java b/src/main/java/com/example/bankcards/entity/CardStatus.java index aba47c8..9ac20eb 100644 --- a/src/main/java/com/example/bankcards/entity/CardStatus.java +++ b/src/main/java/com/example/bankcards/entity/CardStatus.java @@ -1,5 +1,7 @@ package com.example.bankcards.entity; public enum CardStatus { - ACTIVE, BLOCKED, EXPIRED + ACTIVE, + BLOCKED, + EXPIRED, } diff --git a/src/main/java/com/example/bankcards/entity/Role.java b/src/main/java/com/example/bankcards/entity/Role.java index 687a309..b7eeab3 100644 --- a/src/main/java/com/example/bankcards/entity/Role.java +++ b/src/main/java/com/example/bankcards/entity/Role.java @@ -1,6 +1,6 @@ package com.example.bankcards.entity; public enum Role { - ROLE_USER, - ROLE_ADMIN + ROLE_USER, + ROLE_ADMIN, } diff --git a/src/main/java/com/example/bankcards/entity/User.java b/src/main/java/com/example/bankcards/entity/User.java index 8d4f8d2..2d1ed15 100644 --- a/src/main/java/com/example/bankcards/entity/User.java +++ b/src/main/java/com/example/bankcards/entity/User.java @@ -1,6 +1,7 @@ package com.example.bankcards.entity; import jakarta.persistence.*; +import java.util.List; import lombok.*; @Entity @@ -27,4 +28,11 @@ public class User { @Enumerated(EnumType.STRING) @Column(nullable = false) private Role role; + + @OneToMany( + mappedBy = "owner", + cascade = CascadeType.ALL, + fetch = FetchType.LAZY + ) + private List cards; } diff --git a/src/main/java/com/example/bankcards/repository/CardRepository.java b/src/main/java/com/example/bankcards/repository/CardRepository.java index f2a22ff..38ddaf7 100644 --- a/src/main/java/com/example/bankcards/repository/CardRepository.java +++ b/src/main/java/com/example/bankcards/repository/CardRepository.java @@ -2,6 +2,7 @@ package com.example.bankcards.repository; import com.example.bankcards.entity.Card; import com.example.bankcards.entity.CardStatus; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,4 +14,5 @@ public interface CardRepository extends JpaRepository { CardStatus status, Pageable pageable ); + List findByOwnerId(Long ownerId); } diff --git a/src/main/java/com/example/bankcards/service/CardService.java b/src/main/java/com/example/bankcards/service/CardService.java index 62593d6..c147dcb 100644 --- a/src/main/java/com/example/bankcards/service/CardService.java +++ b/src/main/java/com/example/bankcards/service/CardService.java @@ -43,7 +43,8 @@ public class CardService { .status(CardStatus.ACTIVE) .balance(request.getInitialBalance()) .build(); - return toResponse(cardRepository.save(card)); + card = cardRepository.save(card); + return toResponse(card); } public Page getAllCards(Pageable pageable) { diff --git a/src/main/java/com/example/bankcards/util/CardEncryptionUtil.java b/src/main/java/com/example/bankcards/util/CardEncryptionUtil.java index 91f9148..8d6d416 100644 --- a/src/main/java/com/example/bankcards/util/CardEncryptionUtil.java +++ b/src/main/java/com/example/bankcards/util/CardEncryptionUtil.java @@ -49,8 +49,7 @@ public class CardEncryptionUtil { if (cardNumber == null || cardNumber.length() < 4) { return "****"; } - return ( - "**** **** **** " + cardNumber.substring(cardNumber.length() - 4) - ); + String lastFour = cardNumber.substring(cardNumber.length() - 4); + return "**** **** **** " + lastFour; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index d1dffeb..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=bankcards diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f38bd86..4654235 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,10 +3,14 @@ spring: url: jdbc:postgresql://localhost:5432/bankcards username: postgres password: postgres + driver-class-name: org.postgresql.Driver jpa: hibernate: ddl-auto: validate show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect liquibase: change-log: classpath:db/migration/changelog-master.xml @@ -23,3 +27,5 @@ app: springdoc: swagger-ui: path: /swagger-ui.html + api-docs: + path: /v3/api-docs diff --git a/src/test/java/com/example/bankcards/BankcardsApplicationTests.java b/src/test/java/com/example/bankcards/BankcardsApplicationTests.java deleted file mode 100644 index aa1965d..0000000 --- a/src/test/java/com/example/bankcards/BankcardsApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.bankcards; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class BankcardsApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/example/bankcards/controller/AuthControllerTest.java b/src/test/java/com/example/bankcards/controller/AuthControllerTest.java index 9b5853d..f403a03 100644 --- a/src/test/java/com/example/bankcards/controller/AuthControllerTest.java +++ b/src/test/java/com/example/bankcards/controller/AuthControllerTest.java @@ -53,6 +53,7 @@ class AuthControllerTest { @WithMockUser void login_invalidBody_returns400() throws Exception { LoginRequest req = new LoginRequest(); + // username и password — null, должна сработать валидация @NotBlank mockMvc .perform( diff --git a/src/test/java/com/example/bankcards/service/AuthServiceTest.java b/src/test/java/com/example/bankcards/service/AuthServiceTest.java index df84949..25e4495 100644 --- a/src/test/java/com/example/bankcards/service/AuthServiceTest.java +++ b/src/test/java/com/example/bankcards/service/AuthServiceTest.java @@ -76,6 +76,7 @@ class AuthServiceTest { .build(); when(userRepository.save(any())).thenReturn(savedUser); + // UserDetailsService вернёт реальный объект UserDetails с нужным username UserDetails mockDetails = org.springframework.security.core.userdetails.User.withUsername( "newuser" diff --git a/src/test/java/com/example/bankcards/service/CardServiceTest.java b/src/test/java/com/example/bankcards/service/CardServiceTest.java index 8eac649..af976b4 100644 --- a/src/test/java/com/example/bankcards/service/CardServiceTest.java +++ b/src/test/java/com/example/bankcards/service/CardServiceTest.java @@ -1,6 +1,5 @@ package com.example.bankcards.service; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -88,7 +87,7 @@ class CardServiceTest { ); var response = cardService.createCard(req); - assertThat(response).isNotNull(); + assert response != null; verify(cardRepository, times(1)).save(any()); } @@ -141,7 +140,7 @@ class CardServiceTest { ); var response = cardService.blockCard(1L); - assertThat(response).isNotNull(); + assert response != null; verify(cardRepository).save(any()); } }