main
Stepan Pilipenko 1 month ago
parent f4fd3916b8
commit f297b4986c

@ -23,30 +23,32 @@ async def user(request):
try: try:
user1 = dict() user1 = dict()
if request.method == 'POST': if request.method == 'POST':
body: dict = json.loads(request.body)
if body["register"]: if request.body:
token = api.registration(body["register"]["nickname"], body: dict = json.loads(request.body)
body["register"]["password"],
body["register"]["email"]) if body["register"]:
elif body["login"]: token = api.registration(body["register"]["nickname"],
token = api.login(body["login"]["email"], body["register"]["password"],
body["login"]["password"]) body["register"]["email"])
elif body["unregister"]: elif body["login"]:
token = format_token(request.headers.get("Authorization")) token = api.login(body["login"]["email"],
api.unregister(token) body["login"]["password"])
elif body["logout"]: elif body["unregister"]:
token = format_token(request.headers.get("Authorization")) token = format_token(request.headers.get("Authorization"))
api.logout(token) api.unregister(token)
elif body["add_money"]: elif body["logout"]:
token = format_token(request.headers.get("Authorization")) token = format_token(request.headers.get("Authorization"))
api.add_money(token, body["add_money"]["money"]) api.logout(token)
elif body["add_money"]:
token = format_token(request.headers.get("Authorization"))
api.add_money(token, body["add_money"]["money"])
else: else:
token = format_token(request.headers.get("Authorization")) token = format_token(request.headers.get("Authorization"))
user1 = api.get_user(token) user1 = api.get_user(token)
return JsonResponse({"success": list(user1)}, status=200) return JsonResponse({"success": [user1]}, status=200)
except Exception as error: except Exception as error:
return JsonResponse({"error": format(error)}, status=500) return JsonResponse({"error": format(error)}, status=500)

@ -161,7 +161,28 @@ class TestGameShopAPI(unittest.TestCase):
error_msg = get_error_message(basket_resp) error_msg = get_error_message(basket_resp)
self.fail(f"Токен от /login/ не работает: {error_msg}") self.fail(f"Токен от /login/ не работает: {error_msg}")
def test_8_logout_success(self): def test_8_user_success(self):
"""POST /user/ с валидным токеном → возвращает данные пользователя"""
headers = {"Authorization": "Bearer " + self.valid_token}
response = requests.post(f"{BASE_URL}/user/", headers=headers)
if response.status_code != 200:
error_msg = get_error_message(response)
self.fail(f"POST /user/ failed ({response.status_code}): {error_msg}")
data = response.json()
self.assertIn("success", data, f"Ответ не содержит 'success'. Получено: {data}")
user_data = data["success"][0]
required_fields = {"id", "nickname", "email", "token", "money", "histories_id"}
missing = required_fields - set(user_data.keys())
self.assertFalse(missing, f"В данных пользователя отсутствуют поля: {missing}")
self.assertEqual(user_data["nickname"], self.test_nickname)
self.assertEqual(user_data["email"], self.test_email)
self.assertIsInstance(float(user_data["money"]), float)
self.assertIsInstance(user_data["histories_id"], list)
def test_9_logout_success(self):
"""POST /logout/ с валидным токеном — успешный выход""" """POST /logout/ с валидным токеном — успешный выход"""
headers = {"Authorization": "Bearer " + self.valid_token} headers = {"Authorization": "Bearer " + self.valid_token}
response = requests.post(f"{BASE_URL}/logout/", headers=headers) response = requests.post(f"{BASE_URL}/logout/", headers=headers)
@ -173,10 +194,9 @@ class TestGameShopAPI(unittest.TestCase):
self.assertIn("success", data, f"Ответ не содержит 'OK'. Получено: {data}") self.assertIn("success", data, f"Ответ не содержит 'OK'. Получено: {data}")
self.assertEqual(data["success"], self.valid_token, "В ответе должен быть тот же токен") self.assertEqual(data["success"], self.valid_token, "В ответе должен быть тот же токен")
def test_9_all_scenarios_completed(self): def test_10_all_scenarios_completed(self):
"""Финальная проверка: все тесты пройдены""" """Финальная проверка: все тесты пройдены"""
pass pass
if __name__ == "__main__": if __name__ == "__main__":
unittest.main(verbosity=2) unittest.main(verbosity=2)

@ -12,14 +12,14 @@ import {
import { type SyntheticEvent, useCallback } from 'react' import { type SyntheticEvent, useCallback } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { CartButton } from './cart-button' import { CartButton } from './cart-button'
import { useAppDispatch, useAppSelector, selectProduct } from '../storage' import { useAppDispatch, useAppSelector, shop } from '../storage'
export type ProductCardProps = { export type ProductCardProps = {
productId: number productId: number
} }
export const ProductCard = ({ productId }: ProductCardProps) => { export const ProductCard = ({ productId }: ProductCardProps) => {
const product = useAppSelector(selectProduct(productId)) const product = useAppSelector(shop.selector.product(productId))
const { id, name, count, reserved, picture_url, description, type, cost } = product || { const { id, name, count, reserved, picture_url, description, type, cost } = product || {
id: productId, id: productId,
name: '', name: '',

@ -10,14 +10,14 @@ import {
} from '@mui/material' } from '@mui/material'
import { useCallback } from 'react' import { useCallback } from 'react'
import { CartButton } from './cart-button' import { CartButton } from './cart-button'
import { useAppSelector, selectProduct } from '../storage' import { useAppSelector, shop } from '../storage'
export type ProductDetailProps = { export type ProductDetailProps = {
productId: number productId: number
} }
export const ProductDetail = ({ productId }: ProductDetailProps) => { export const ProductDetail = ({ productId }: ProductDetailProps) => {
const product = useAppSelector(selectProduct(productId)) const product = useAppSelector(shop.selector.product(productId))
const { id, name, count, reserved, picture_url, description, type, cost } = product || { const { id, name, count, reserved, picture_url, description, type, cost } = product || {
id: productId, id: productId,
name: '', name: '',

@ -1,19 +1,19 @@
import { Grid } from '@mui/material' import { Grid } from '@mui/material'
import { useCallback, useState, type JSX } from 'react' import { useCallback, useState, type JSX } from 'react'
import { ProductCard } from './product-card' import { ProductCard } from './product-card'
import { type Product } from '../types' import { type ProductType } from '../types'
import { LoadMore } from './load-more' import { LoadMore } from './load-more'
import { useAppSelector, selectProducts } from '../storage' import { useAppSelector, shop } from '../storage'
export type ProductListProps = { export type ProductListProps = {
products: Product[] products: ProductType[]
loading?: boolean loading?: boolean
} }
export const ProductList = ({ products, loading = false }: ProductListProps): JSX.Element => { export const ProductList = ({ products, loading = false }: ProductListProps): JSX.Element => {
const [page, setPage] = useState<number>(1) const [page, setPage] = useState<number>(1)
const productsLength = useAppSelector(selectProducts).length const productsLength = useAppSelector(shop.selector.products).length
const isEndOfList = products && products.length >= productsLength const isEndOfList = products && products.length >= productsLength
const loadMorePosts = useCallback(() => { const loadMorePosts = useCallback(() => {

@ -5,15 +5,15 @@ import { useEffect, type JSX } from 'react'
import { Header } from './header' import { Header } from './header'
import { Footer } from './footer' import { Footer } from './footer'
import { Body } from './body' import { Body } from './body'
import { fetchProducts, selectShopLoading, useAppDispatch, useAppSelector } from '../storage' import { shop, useAppDispatch, useAppSelector } from '../storage'
export const Layout = (): JSX.Element => { export const Layout = (): JSX.Element => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const loading = useAppSelector(selectShopLoading) const loading = useAppSelector(shop.selector.loading)
useEffect(() => { useEffect(() => {
dispatch(fetchProducts()) dispatch(shop.fetch.products())
}, []) }, [])
return ( return (

@ -16,33 +16,44 @@ const DEFAULT_HEADERS: HeaderType = {
} }
export interface NetworkApi { export interface NetworkApi {
setAccessToken: (token: string) => void get token(): string
onResponse: <T>(res: Response) => Promise<T> set token(value: string)
request: <T>(endpoint: string, options?: RequestInit) => Promise<T> request: <T>(endpoint: string, options?: RequestInit) => Promise<T>
} }
class Network implements NetworkApi { class Network implements NetworkApi {
private baseUrl: string private _baseUrl: string
private headers: HeaderType private _headers: HeaderType
private _token: string
constructor() { constructor() {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
this.baseUrl = DEV_URL this._baseUrl = DEV_URL
} else { } else {
this.baseUrl = PROD_URL this._baseUrl = PROD_URL
} }
this.headers = DEFAULT_HEADERS this._headers = DEFAULT_HEADERS
this._token = ''
} }
private getStringURL(endpoint: string): string { private getStringURL(endpoint: string): string {
return `${this.baseUrl}${endpoint.replace(/^\//, '')}` return `${this._baseUrl}/${endpoint.replace(/^\//, '')}`
} }
setAccessToken(token: string): void { get token(): string {
this.headers.authorization = token return this._token
} }
async onResponse<T>(res: Response): Promise<T> { set token(value: string) {
if (value) {
this._headers.authorization = `Bearer ${value}`
} else {
this._headers.authorization = ''
}
this._token = value
}
private async onResponse<T>(res: Response): Promise<T> {
return res.ok ? res.json() : res.json().then(data => Promise.reject(data)) return res.ok ? res.json() : res.json().then(data => Promise.reject(data))
} }
@ -50,9 +61,8 @@ class Network implements NetworkApi {
const res = await fetch(this.getStringURL(endpoint), { const res = await fetch(this.getStringURL(endpoint), {
method: 'GET', method: 'GET',
...options, ...options,
headers: { ...this.headers, ...options?.headers }, headers: { ...this._headers, ...options?.headers },
}) })
return await this.onResponse<T>(res) return await this.onResponse<T>(res)
} }
} }

@ -1,4 +1,4 @@
import type { JSX } from 'react' import { useEffect, type JSX } from 'react'
import { Avatar, Box, Container, TextField, Typography, Button, Link } from '@mui/material' import { Avatar, Box, Container, TextField, Typography, Button, Link } from '@mui/material'
import KeyIcon from '@mui/icons-material/Key' import KeyIcon from '@mui/icons-material/Key'
import { Controller, type SubmitHandler, useForm } from 'react-hook-form' import { Controller, type SubmitHandler, useForm } from 'react-hook-form'
@ -6,7 +6,7 @@ import { yupResolver } from '@hookform/resolvers/yup'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom' import { Link as RouterLink, useLocation, useNavigate } from 'react-router-dom'
import * as yup from 'yup' import * as yup from 'yup'
import { useAppDispatch } from '../storage' import { auth, useAppDispatch, useAppSelector } from '../storage'
import type { ErrorType } from '../types' import type { ErrorType } from '../types'
export interface LoginFormValues { export interface LoginFormValues {
@ -21,10 +21,23 @@ export const loginFormSchema = yup.object({
export const LoginPage = (): JSX.Element => { export const LoginPage = (): JSX.Element => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const token = useAppSelector(auth.selector.accessToken)
const error = useAppSelector(auth.selector.error)
const { state } = useLocation() const { state } = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
useEffect(() => {
if (token && !error) {
toast.success('Вы успешно вошли в систему!')
// переходит туда откуда выпали на логин
navigate(state.from)
}
if (error) {
toast.error(error)
}
}, [token, error])
// инициализируем react-hook-form // инициализируем react-hook-form
const { const {
// control понадобиться, чтобы подружить react-hook-form и компоненты из MUI // control понадобиться, чтобы подружить react-hook-form и компоненты из MUI
@ -44,10 +57,7 @@ export const LoginPage = (): JSX.Element => {
const submitHandler: SubmitHandler<LoginFormValues> = async values => { const submitHandler: SubmitHandler<LoginFormValues> = async values => {
try { try {
// TODO: логинемся и запрашиваем все данные dispatch(auth.fetch.login({ email: values.email, password: values.password }))
toast.success('Вы успешно вошли в систему!')
// переходит туда откуда выпали на логин
navigate(state.from)
} catch (error: unknown) { } catch (error: unknown) {
// Если произошла ошибка, то выводим уведомление // Если произошла ошибка, то выводим уведомление
const errorText = (error as ErrorType).error const errorText = (error as ErrorType).error

@ -1,11 +1,10 @@
import { memo } from 'react' import { memo } from 'react'
import { Container } from '@mui/material' import { Container } from '@mui/material'
import { useAppSelector } from '../storage' import { useAppSelector, shop } from '../storage'
import { selectProducts } from '../storage/shop-slice'
import { ProductList } from '../components' import { ProductList } from '../components'
export const ShopPage = memo(() => { export const ShopPage = memo(() => {
const products = useAppSelector(selectProducts) const products = useAppSelector(shop.selector.products)
return ( return (
<Container disableGutters component="main"> <Container disableGutters component="main">

@ -0,0 +1,115 @@
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { createAppAsyncThunk } from './hooks'
import { networkApi, type ResponseData } from '../network'
interface AuthState {
accessToken: string
loading: boolean
error?: string
}
const initialState: AuthState = {
accessToken: '',
loading: false,
error: undefined,
}
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setAccessToken(state, action: PayloadAction<AuthState>) {
state.accessToken = action.payload.accessToken
},
clearToken() {
return initialState
},
},
extraReducers: builder => {
builder
.addCase(fetchLogin.pending, state => {
state.loading = true
state.error = undefined
})
.addCase(fetchLogin.fulfilled, (state, action) => {
state.accessToken = String(action.payload.success?.[0]) || ''
state.loading = false
state.error = undefined
networkApi.token = state.accessToken
})
.addCase(fetchLogin.rejected, (state, action) => {
state.loading = false
if (action.payload) {
state.error = (action.payload as ResponseData).error
} else {
state.error = action.error.message
}
})
.addCase(fetchLogout.pending, state => {
state.loading = true
state.error = undefined
})
.addCase(fetchLogout.fulfilled, state => {
state.accessToken = ''
state.loading = false
state.error = undefined
networkApi.token = ''
})
.addCase(fetchLogout.rejected, (state, action) => {
state.loading = false
if (action.payload) {
state.error = (action.payload as ResponseData).error
} else {
state.error = action.error.message
}
})
},
selectors: {
accessToken: (state: AuthState) => state.accessToken,
loading: (state: AuthState) => state.loading,
error: (state: AuthState) => state.error,
},
})
type LoginProps = {
email: string
password: string
}
export const fetchLogin = createAppAsyncThunk<ResponseData, LoginProps>(
`${authSlice.name}/fetchLogin`,
async (props, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/login/', {
method: 'POST',
body: JSON.stringify(props),
})
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
export const fetchLogout = createAppAsyncThunk<ResponseData>(
`${authSlice.name}/fetchLogout`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/logout/')
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
class Auth {
selector = { ...authSlice.selectors }
action = authSlice.actions
reducer = authSlice.reducer
name = authSlice.name
fetch = { login: fetchLogin, logout: fetchLogout }
}
export const auth = new Auth()

@ -1,5 +1,14 @@
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import type { AppDispatch, RootState } from '../store' import type { AppDispatch, RootState } from '../store'
import { createAsyncThunk } from '@reduxjs/toolkit'
import type { NetworkApi } from '../network'
export const useAppDispatch = useDispatch.withTypes<AppDispatch>() export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
export const useAppSelector = useSelector.withTypes<RootState>() export const useAppSelector = useSelector.withTypes<RootState>()
export const createAppAsyncThunk = createAsyncThunk.withTypes<{
state: RootState
getState: () => RootState
dispatch: AppDispatch
extra: NetworkApi
}>()

@ -1,2 +1,4 @@
export * from './shop-slice' export * from './shop-slice'
export * from './auth-slice'
export * from './user-slice'
export * from './hooks' export * from './hooks'

@ -1,7 +1,7 @@
import { createAsyncThunk, createSlice, type PayloadAction, createSelector } from '@reduxjs/toolkit' import { createSlice, type PayloadAction, createSelector } from '@reduxjs/toolkit'
import type { RootState, ThunkApi } from '../store' import type { ProductType } from '../types'
import type { Product } from '../types'
import type { ResponseData } from '../network' import type { ResponseData } from '../network'
import { createAppAsyncThunk } from './hooks'
interface AddBasketAction { interface AddBasketAction {
id: number id: number
@ -9,7 +9,7 @@ interface AddBasketAction {
} }
interface ShopState { interface ShopState {
products: Product[] products: ProductType[]
loading: boolean loading: boolean
error?: string error?: string
} }
@ -24,10 +24,13 @@ export const shopSlice = createSlice({
name: 'shop', name: 'shop',
initialState, initialState,
reducers: { reducers: {
// TODO: дописать action.payload.id
addToBasket: (state, action: PayloadAction<AddBasketAction>) => { addToBasket: (state, action: PayloadAction<AddBasketAction>) => {
state.products[0].count -= action.payload.count for (const product of state.products) {
state.products[0].reserved += action.payload.count if (product.id === action.payload.id) {
product.count -= action.payload.count
product.reserved += action.payload.count
}
}
}, },
}, },
extraReducers: builder => { extraReducers: builder => {
@ -37,7 +40,7 @@ export const shopSlice = createSlice({
state.error = undefined state.error = undefined
}) })
.addCase(fetchProducts.fulfilled, (state, action) => { .addCase(fetchProducts.fulfilled, (state, action) => {
state.products = action.payload.success as Product[] state.products = action.payload.success as ProductType[]
state.loading = false state.loading = false
}) })
.addCase(fetchProducts.rejected, (state, action) => { .addCase(fetchProducts.rejected, (state, action) => {
@ -49,23 +52,20 @@ export const shopSlice = createSlice({
} }
}) })
}, },
selectors: {
products: (state: ShopState) => state.products,
loading: (state: ShopState) => state.loading,
error: (state: ShopState) => state.error,
},
}) })
export const { addToBasket } = shopSlice.actions const selectProduct = (productId: number) =>
createSelector([shopSlice.selectors.products], products => {
export const selectShopLoading = (state: RootState) => state.shop.loading
export const selectShopError = (state: RootState) => state.shop.error
export const selectProducts = (state: RootState) => state.shop.products
export const selectProduct = (productId: number) =>
createSelector([selectProducts], products => {
return products.find(product => product.id === productId) return products.find(product => product.id === productId)
}) })
export const shopReducer = shopSlice.reducer export const fetchProducts = createAppAsyncThunk<ResponseData>(
`${shopSlice.name}/fetchProducts`,
export const fetchProducts = createAsyncThunk<ResponseData, void, ThunkApi>(
'products/fetchProducts',
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => { async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try { try {
const data = await networkApi.request<ResponseData>('/shop/') const data = await networkApi.request<ResponseData>('/shop/')
@ -75,3 +75,13 @@ export const fetchProducts = createAsyncThunk<ResponseData, void, ThunkApi>(
} }
} }
) )
class Shop {
selector = { ...shopSlice.selectors, product: selectProduct }
action = shopSlice.actions
reducer = shopSlice.reducer
name = shopSlice.name
fetch = { products: fetchProducts }
}
export const shop = new Shop()

@ -0,0 +1,77 @@
import { type PayloadAction, createSlice } from '@reduxjs/toolkit'
import { type UserType } from '../types'
import { createAppAsyncThunk } from './hooks'
import type { ResponseData } from '../network'
type UserState = {
user?: UserType
loading: boolean
error?: string
}
const initialState: UserState = {
user: undefined,
loading: false,
error: undefined,
}
export const userSliceName = 'user'
const userSlice = createSlice({
name: userSliceName,
initialState,
reducers: {
setUser: (state, action: PayloadAction<UserType>) => {
state.user = action.payload
},
clearUser() {
return initialState
},
},
extraReducers: builder => {
builder
.addCase(fetchUser.pending, state => {
state.loading = true
state.error = undefined
})
.addCase(fetchUser.fulfilled, (state, action) => {
state.user = action.payload.success?.[0] as UserType
state.loading = false
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false
if (action.payload) {
state.error = (action.payload as ResponseData).error
} else {
state.error = action.error.message
}
})
},
selectors: {
user: (state: UserState) => state.user,
loading: (state: UserState) => state.loading,
error: (state: UserState) => state.error,
},
})
export const fetchUser = createAppAsyncThunk<ResponseData>(
`${userSliceName}/fetchUser`,
async (_, { fulfillWithValue, rejectWithValue, extra: networkApi }) => {
try {
const data = await networkApi.request<ResponseData>('/user/')
return fulfillWithValue(data)
} catch (error) {
return rejectWithValue(error)
}
}
)
class User {
selector = { ...userSlice.selectors }
action = userSlice.actions
reducer = userSlice.reducer
name = userSlice.name
fetch = { user: fetchUser }
}
export const user = new User()

@ -1,11 +1,15 @@
import { configureStore } from '@reduxjs/toolkit' import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { networkApi, type NetworkApi } from './network' import { networkApi, type NetworkApi } from './network'
import { shopReducer } from './storage' import { shop, auth, user } from './storage'
const reducer = combineReducers({
[auth.name]: auth.reducer,
[shop.name]: shop.reducer,
[user.name]: user.reducer,
})
export const store = configureStore({ export const store = configureStore({
reducer: { reducer,
shop: shopReducer,
},
middleware: getDefaultMiddleware => middleware: getDefaultMiddleware =>
getDefaultMiddleware({ getDefaultMiddleware({
thunk: { thunk: {

@ -1,3 +1,4 @@
export * from './product' export * from './product'
export * from './error' export * from './error'
export * from './clean-component' export * from './clean-component'
export * from './user'

@ -1,4 +1,4 @@
export interface Product { export type ProductType = {
id: number id: number
name: string name: string
count: number count: number

@ -0,0 +1,9 @@
export type UserType = {
id: number
nickname: string
email: string
token: string
token_expiry_date: Date
money: string
histories_id: number[]
}
Loading…
Cancel
Save