新增点赞收藏功能:cloud_favorites 模型、查询/设置点赞状态及收藏列表接口
This commit is contained in:
@@ -29,6 +29,7 @@ app/
|
|||||||
clouds.py # 云图
|
clouds.py # 云图
|
||||||
cloud_types.py # 云类型
|
cloud_types.py # 云类型
|
||||||
collections.py # 图鉴
|
collections.py # 图鉴
|
||||||
|
favorites.py # 照片点赞收藏
|
||||||
profiles.py # 用户资料
|
profiles.py # 用户资料
|
||||||
admin.py # 管理后台
|
admin.py # 管理后台
|
||||||
health.py # 健康检查
|
health.py # 健康检查
|
||||||
@@ -159,6 +160,42 @@ fetch(url, { credentials: 'include' })
|
|||||||
|
|
||||||
seed 会写入 10 种标准云类型(积云、层云、卷云等),含中英文名、云族与稀有度。
|
seed 会写入 10 种标准云类型(积云、层云、卷云等),含中英文名、云族与稀有度。
|
||||||
|
|
||||||
|
### 点赞收藏
|
||||||
|
|
||||||
|
对照片的点赞/收藏(与云类型「图鉴」不同)。数据表 `cloud_favorites`:`(user_id, cloud_id)` 唯一。
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `GET` | `/api/v1/clouds/{id}/favorite` | 查询喜欢状态(需登录) |
|
||||||
|
| `PUT` | `/api/v1/clouds/{id}/favorite` | 设置或取消收藏(需登录) |
|
||||||
|
| `GET` | `/api/v1/favorites/me` | 当前用户收藏列表分页(按收藏时间倒序) |
|
||||||
|
|
||||||
|
查询喜欢状态(默认当前用户;管理员可传 `user_id` 查他人):
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/clouds/{id}/favorite
|
||||||
|
GET /api/v1/clouds/{id}/favorite?user_id=<uuid>
|
||||||
|
```
|
||||||
|
|
||||||
|
设置/取消请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "favorited": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
响应:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cloud_id": "…",
|
||||||
|
"user_id": "…",
|
||||||
|
"favorited": true,
|
||||||
|
"favorite_count": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
可见性与查看云图一致:公开已审核照片,或本人/管理员可见的待审、隐藏照片。重复收藏为幂等;删除用户或云图时收藏记录级联删除。普通用户只能查自己的喜欢状态。
|
||||||
|
|
||||||
### 用户资料
|
### 用户资料
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
@@ -269,11 +306,12 @@ uv run alembic upgrade head --sql
|
|||||||
- `tests/test_auth.py`:注册、邮箱确认、登录、刷新、登出、改密
|
- `tests/test_auth.py`:注册、邮箱确认、登录、刷新、登出、改密
|
||||||
- `tests/test_clouds.py`:上传、缩略图、画廊、地图、更新与删除
|
- `tests/test_clouds.py`:上传、缩略图、画廊、地图、更新与删除
|
||||||
- `tests/test_collections.py`:图鉴解锁
|
- `tests/test_collections.py`:图鉴解锁
|
||||||
|
- `tests/test_favorites.py`:照片点赞收藏与取消、收藏列表
|
||||||
- `tests/test_cloud_types.py`:云类型查询
|
- `tests/test_cloud_types.py`:云类型查询
|
||||||
- `tests/test_profiles.py`:资料、用户云图、管理员 stats
|
- `tests/test_profiles.py`:资料、用户云图、管理员 stats
|
||||||
- `tests/test_admin.py`:统计、用户管理、审核与批量操作
|
- `tests/test_admin.py`:统计、用户管理、审核与批量操作
|
||||||
|
|
||||||
当前约 80 个用例。生产环境仍建议在真实 PostgreSQL 上再跑一遍迁移与关键路径。
|
生产环境仍建议在真实 PostgreSQL 上再跑一遍迁移与关键路径。
|
||||||
|
|
||||||
## 部署注意事项
|
## 部署注意事项
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Add cloud_favorites table for photo likes/favorites.
|
||||||
|
|
||||||
|
Revision ID: 20260726_0002
|
||||||
|
Revises: 20260718_0001
|
||||||
|
Create Date: 2026-07-26
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision: str = "20260726_0002"
|
||||||
|
down_revision: str | None = "20260718_0001"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"cloud_favorites",
|
||||||
|
sa.Column("id", sa.Uuid(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.Uuid(), nullable=False),
|
||||||
|
sa.Column("cloud_id", sa.Uuid(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"created_at",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.ForeignKeyConstraint(["cloud_id"], ["clouds.id"], ondelete="CASCADE"),
|
||||||
|
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("user_id", "cloud_id", name="uq_cloud_favorites_user_cloud"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_cloud_favorites_user_id", "cloud_favorites", ["user_id"])
|
||||||
|
op.create_index("ix_cloud_favorites_cloud_id", "cloud_favorites", ["cloud_id"])
|
||||||
|
op.create_index(
|
||||||
|
"ix_cloud_favorites_user_created",
|
||||||
|
"cloud_favorites",
|
||||||
|
["user_id", sa.text("created_at DESC"), sa.text("id DESC")],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_cloud_favorites_user_created", table_name="cloud_favorites")
|
||||||
|
op.drop_index("ix_cloud_favorites_cloud_id", table_name="cloud_favorites")
|
||||||
|
op.drop_index("ix_cloud_favorites_user_id", table_name="cloud_favorites")
|
||||||
|
op.drop_table("cloud_favorites")
|
||||||
+2
-1
@@ -6,7 +6,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from app.config import settings
|
from app.config import settings
|
||||||
from app.routers import admin, auth, cloud_types, clouds, collections, health, profiles
|
from app.routers import admin, auth, cloud_types, clouds, collections, favorites, health, profiles
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -36,6 +36,7 @@ for router in (
|
|||||||
cloud_types.router,
|
cloud_types.router,
|
||||||
clouds.router,
|
clouds.router,
|
||||||
collections.router,
|
collections.router,
|
||||||
|
favorites.router,
|
||||||
admin.router,
|
admin.router,
|
||||||
):
|
):
|
||||||
app.include_router(router, prefix=settings.api_v1_prefix)
|
app.include_router(router, prefix=settings.api_v1_prefix)
|
||||||
|
|||||||
@@ -236,3 +236,33 @@ class UserCollection(Base):
|
|||||||
|
|
||||||
cloud_type: Mapped[CloudType] = relationship(lazy="raise")
|
cloud_type: Mapped[CloudType] = relationship(lazy="raise")
|
||||||
first_cloud: Mapped[Cloud | None] = relationship(lazy="raise")
|
first_cloud: Mapped[Cloud | None] = relationship(lazy="raise")
|
||||||
|
|
||||||
|
|
||||||
|
class CloudFavorite(Base):
|
||||||
|
"""用户对照片的点赞/收藏记录。"""
|
||||||
|
|
||||||
|
__tablename__ = "cloud_favorites"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint("user_id", "cloud_id", name="uq_cloud_favorites_user_cloud"),
|
||||||
|
Index("ix_cloud_favorites_user_id", "user_id"),
|
||||||
|
Index("ix_cloud_favorites_cloud_id", "cloud_id"),
|
||||||
|
Index(
|
||||||
|
"ix_cloud_favorites_user_created",
|
||||||
|
"user_id",
|
||||||
|
text("created_at DESC"),
|
||||||
|
text("id DESC"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(Uuid, primary_key=True, default=uuid.uuid4)
|
||||||
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
Uuid, ForeignKey("users.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
|
cloud_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
Uuid, ForeignKey("clouds.id", ondelete="CASCADE"), nullable=False
|
||||||
|
)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
cloud: Mapped[Cloud] = relationship(lazy="raise")
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import math
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, Query, status
|
||||||
|
from sqlalchemy import delete, func, select
|
||||||
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||||
|
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
|
||||||
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
|
from app.deps import CurrentUser, DbSession
|
||||||
|
from app.models import Cloud, CloudFavorite, User
|
||||||
|
from app.schemas import CloudOut, FavoriteIn, FavoriteOut, PageOut
|
||||||
|
from app.serializers import cloud_out
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(tags=["点赞收藏"])
|
||||||
|
|
||||||
|
|
||||||
|
def _cloud_options():
|
||||||
|
return (
|
||||||
|
joinedload(Cloud.user).joinedload(User.profile),
|
||||||
|
joinedload(Cloud.cloud_type),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_visible_cloud(
|
||||||
|
db: DbSession,
|
||||||
|
cloud_id: uuid.UUID,
|
||||||
|
user: CurrentUser,
|
||||||
|
) -> Cloud:
|
||||||
|
result = await db.execute(
|
||||||
|
select(Cloud).options(*_cloud_options()).where(Cloud.id == cloud_id)
|
||||||
|
)
|
||||||
|
cloud = result.scalar_one_or_none()
|
||||||
|
if not cloud:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
|
||||||
|
|
||||||
|
can_manage = cloud.user_id == user.id or user.profile.role == "admin"
|
||||||
|
if not can_manage and (cloud.status != "approved" or cloud.is_hidden):
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="图片不存在")
|
||||||
|
return cloud
|
||||||
|
|
||||||
|
|
||||||
|
async def _favorite_count(db: DbSession, cloud_id: uuid.UUID) -> int:
|
||||||
|
return (
|
||||||
|
await db.scalar(
|
||||||
|
select(func.count())
|
||||||
|
.select_from(CloudFavorite)
|
||||||
|
.where(CloudFavorite.cloud_id == cloud_id)
|
||||||
|
)
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _is_favorited(db: DbSession, user_id: uuid.UUID, cloud_id: uuid.UUID) -> bool:
|
||||||
|
favorite_id = await db.scalar(
|
||||||
|
select(CloudFavorite.id).where(
|
||||||
|
CloudFavorite.user_id == user_id,
|
||||||
|
CloudFavorite.cloud_id == cloud_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return favorite_id is not None
|
||||||
|
|
||||||
|
|
||||||
|
async def _favorite_out(
|
||||||
|
db: DbSession,
|
||||||
|
*,
|
||||||
|
cloud_id: uuid.UUID,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
favorited: bool | None = None,
|
||||||
|
) -> FavoriteOut:
|
||||||
|
if favorited is None:
|
||||||
|
favorited = await _is_favorited(db, user_id, cloud_id)
|
||||||
|
return FavoriteOut(
|
||||||
|
cloud_id=cloud_id,
|
||||||
|
user_id=user_id,
|
||||||
|
favorited=favorited,
|
||||||
|
favorite_count=await _favorite_count(db, cloud_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/clouds/{cloud_id}/favorite", response_model=FavoriteOut)
|
||||||
|
async def get_cloud_favorite(
|
||||||
|
cloud_id: uuid.UUID,
|
||||||
|
user: CurrentUser,
|
||||||
|
db: DbSession,
|
||||||
|
user_id: uuid.UUID | None = Query(
|
||||||
|
None,
|
||||||
|
description="要查询的用户 ID;默认当前登录用户。仅本人或管理员可查他人。",
|
||||||
|
),
|
||||||
|
) -> FavoriteOut:
|
||||||
|
"""查询某用户对某张照片的点赞/收藏状态。"""
|
||||||
|
await _get_visible_cloud(db, cloud_id, user)
|
||||||
|
|
||||||
|
target_user_id = user_id or user.id
|
||||||
|
if target_user_id != user.id and user.profile.role != "admin":
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="只能查询自己的喜欢状态",
|
||||||
|
)
|
||||||
|
|
||||||
|
return await _favorite_out(db, cloud_id=cloud_id, user_id=target_user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/clouds/{cloud_id}/favorite", response_model=FavoriteOut)
|
||||||
|
async def set_cloud_favorite(
|
||||||
|
cloud_id: uuid.UUID,
|
||||||
|
payload: FavoriteIn,
|
||||||
|
user: CurrentUser,
|
||||||
|
db: DbSession,
|
||||||
|
) -> FavoriteOut:
|
||||||
|
"""点赞/收藏照片,或取消。"""
|
||||||
|
await _get_visible_cloud(db, cloud_id, user)
|
||||||
|
|
||||||
|
if payload.favorited:
|
||||||
|
insert_factory = (
|
||||||
|
pg_insert if db.bind and db.bind.dialect.name == "postgresql" else sqlite_insert
|
||||||
|
)
|
||||||
|
await db.execute(
|
||||||
|
insert_factory(CloudFavorite)
|
||||||
|
.values(user_id=user.id, cloud_id=cloud_id)
|
||||||
|
.on_conflict_do_nothing(
|
||||||
|
index_elements=[CloudFavorite.user_id, CloudFavorite.cloud_id]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await db.execute(
|
||||||
|
delete(CloudFavorite).where(
|
||||||
|
CloudFavorite.user_id == user.id,
|
||||||
|
CloudFavorite.cloud_id == cloud_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
return await _favorite_out(
|
||||||
|
db,
|
||||||
|
cloud_id=cloud_id,
|
||||||
|
user_id=user.id,
|
||||||
|
favorited=payload.favorited,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/favorites/me", response_model=PageOut[CloudOut])
|
||||||
|
async def list_my_favorites(
|
||||||
|
user: CurrentUser,
|
||||||
|
db: DbSession,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(50, ge=1, le=100),
|
||||||
|
) -> PageOut[CloudOut]:
|
||||||
|
"""当前用户点赞收藏的照片列表(按收藏时间倒序)。"""
|
||||||
|
total = (
|
||||||
|
await db.scalar(
|
||||||
|
select(func.count())
|
||||||
|
.select_from(CloudFavorite)
|
||||||
|
.where(CloudFavorite.user_id == user.id)
|
||||||
|
)
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
result = await db.execute(
|
||||||
|
select(Cloud)
|
||||||
|
.join(CloudFavorite, CloudFavorite.cloud_id == Cloud.id)
|
||||||
|
.options(*_cloud_options())
|
||||||
|
.where(CloudFavorite.user_id == user.id)
|
||||||
|
.order_by(CloudFavorite.created_at.desc(), CloudFavorite.id.desc())
|
||||||
|
.offset((page - 1) * page_size)
|
||||||
|
.limit(page_size)
|
||||||
|
)
|
||||||
|
items = [
|
||||||
|
cloud_out(item, include_private=item.user_id == user.id or user.profile.role == "admin")
|
||||||
|
for item in result.scalars().unique().all()
|
||||||
|
]
|
||||||
|
return PageOut(
|
||||||
|
items=items,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
total=total,
|
||||||
|
total_pages=max(1, math.ceil(total / page_size)) if total else 1,
|
||||||
|
)
|
||||||
@@ -262,3 +262,16 @@ class ProfileStatsOut(BaseModel):
|
|||||||
cloud_count: int
|
cloud_count: int
|
||||||
public_cloud_count: int
|
public_cloud_count: int
|
||||||
collection_count: int
|
collection_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class FavoriteIn(BaseModel):
|
||||||
|
"""设置或取消对照片的点赞收藏。"""
|
||||||
|
|
||||||
|
favorited: bool
|
||||||
|
|
||||||
|
|
||||||
|
class FavoriteOut(BaseModel):
|
||||||
|
cloud_id: uuid.UUID
|
||||||
|
user_id: uuid.UUID
|
||||||
|
favorited: bool
|
||||||
|
favorite_count: int
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from tests.conftest import (
|
||||||
|
admin_token,
|
||||||
|
approve_cloud,
|
||||||
|
bearer,
|
||||||
|
register_and_confirm,
|
||||||
|
upload_cloud,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_favorite(client: TestClient, token: str, cloud_id: str, favorited: bool):
|
||||||
|
return client.put(
|
||||||
|
f"/api/v1/clouds/{cloud_id}/favorite",
|
||||||
|
headers=bearer(token),
|
||||||
|
json={"favorited": favorited},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_favorite(client: TestClient, token: str, cloud_id: str, **params):
|
||||||
|
return client.get(
|
||||||
|
f"/api/v1/clouds/{cloud_id}/favorite",
|
||||||
|
headers=bearer(token),
|
||||||
|
params=params or None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_favorite_and_unfavorite_public_cloud(client: TestClient, monkeypatch) -> None:
|
||||||
|
owner = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"fav-owner-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"favowner12",
|
||||||
|
"收藏主人",
|
||||||
|
)
|
||||||
|
fan = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"fav-fan-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"favfan1234",
|
||||||
|
"点赞粉丝",
|
||||||
|
)
|
||||||
|
upload = upload_cloud(client, owner["token"], cloud_type_id="1")
|
||||||
|
cloud_id = upload.json()["cloud"]["id"]
|
||||||
|
approve_cloud(client, admin_token(client), cloud_id)
|
||||||
|
|
||||||
|
r = _get_favorite(client, fan["token"], cloud_id)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["favorited"] is False
|
||||||
|
assert r.json()["favorite_count"] == 0
|
||||||
|
assert r.json()["user_id"] == fan["user_id"]
|
||||||
|
|
||||||
|
r = _set_favorite(client, fan["token"], cloud_id, True)
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert body["cloud_id"] == cloud_id
|
||||||
|
assert body["user_id"] == fan["user_id"]
|
||||||
|
assert body["favorited"] is True
|
||||||
|
assert body["favorite_count"] == 1
|
||||||
|
|
||||||
|
r = _get_favorite(client, fan["token"], cloud_id)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["favorited"] is True
|
||||||
|
assert r.json()["favorite_count"] == 1
|
||||||
|
|
||||||
|
# idempotent re-favorite
|
||||||
|
r = _set_favorite(client, fan["token"], cloud_id, True)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["favorited"] is True
|
||||||
|
assert r.json()["favorite_count"] == 1
|
||||||
|
|
||||||
|
r = _set_favorite(client, fan["token"], cloud_id, False)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["favorited"] is False
|
||||||
|
assert r.json()["favorite_count"] == 0
|
||||||
|
|
||||||
|
r = _get_favorite(client, fan["token"], cloud_id)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["favorited"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_favorite_for_other_user_forbidden(client: TestClient, monkeypatch) -> None:
|
||||||
|
owner = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"other-owner-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"otherowner",
|
||||||
|
"他人主人",
|
||||||
|
)
|
||||||
|
fan = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"other-fan-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"otherfan12",
|
||||||
|
"他人粉丝",
|
||||||
|
)
|
||||||
|
upload = upload_cloud(client, owner["token"], cloud_type_id="1")
|
||||||
|
cloud_id = upload.json()["cloud"]["id"]
|
||||||
|
approve_cloud(client, admin_token(client), cloud_id)
|
||||||
|
assert _set_favorite(client, fan["token"], cloud_id, True).status_code == 200
|
||||||
|
|
||||||
|
# 普通用户不能查别人的喜欢状态
|
||||||
|
r = _get_favorite(client, owner["token"], cloud_id, user_id=fan["user_id"])
|
||||||
|
assert r.status_code == 403
|
||||||
|
|
||||||
|
# 管理员可以
|
||||||
|
r = _get_favorite(client, admin_token(client), cloud_id, user_id=fan["user_id"])
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["user_id"] == fan["user_id"]
|
||||||
|
assert r.json()["favorited"] is True
|
||||||
|
assert r.json()["favorite_count"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_favorite_unauthenticated(client: TestClient) -> None:
|
||||||
|
r = client.get(f"/api/v1/clouds/{uuid.uuid4()}/favorite")
|
||||||
|
assert r.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_favorite_unauthenticated(client: TestClient) -> None:
|
||||||
|
r = client.put(
|
||||||
|
f"/api/v1/clouds/{uuid.uuid4()}/favorite",
|
||||||
|
json={"favorited": True},
|
||||||
|
)
|
||||||
|
assert r.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_cannot_favorite_pending_cloud_of_others(client: TestClient, monkeypatch) -> None:
|
||||||
|
owner = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"pending-owner-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"pendingown",
|
||||||
|
"待审主人",
|
||||||
|
)
|
||||||
|
fan = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"pending-fan-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"pendingfan",
|
||||||
|
"待审粉丝",
|
||||||
|
)
|
||||||
|
upload = upload_cloud(client, owner["token"], cloud_type_id="2")
|
||||||
|
cloud_id = upload.json()["cloud"]["id"]
|
||||||
|
|
||||||
|
r = _set_favorite(client, fan["token"], cloud_id, True)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_owner_can_favorite_own_pending_cloud(client: TestClient, monkeypatch) -> None:
|
||||||
|
owner = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"own-fav-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"ownfavpass",
|
||||||
|
"自赞用户",
|
||||||
|
)
|
||||||
|
upload = upload_cloud(client, owner["token"], cloud_type_id="3")
|
||||||
|
cloud_id = upload.json()["cloud"]["id"]
|
||||||
|
|
||||||
|
r = _set_favorite(client, owner["token"], cloud_id, True)
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["favorited"] is True
|
||||||
|
assert r.json()["favorite_count"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_my_favorites(client: TestClient, monkeypatch) -> None:
|
||||||
|
owner = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"list-owner-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"listowner1",
|
||||||
|
"列表主人",
|
||||||
|
)
|
||||||
|
fan = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"list-fan-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"listfan123",
|
||||||
|
"列表粉丝",
|
||||||
|
)
|
||||||
|
ids = []
|
||||||
|
for type_id in ("1", "2"):
|
||||||
|
upload = upload_cloud(client, owner["token"], cloud_type_id=type_id)
|
||||||
|
cloud_id = upload.json()["cloud"]["id"]
|
||||||
|
approve_cloud(client, admin_token(client), cloud_id)
|
||||||
|
ids.append(cloud_id)
|
||||||
|
assert _set_favorite(client, fan["token"], cloud_id, True).status_code == 200
|
||||||
|
|
||||||
|
r = client.get("/api/v1/favorites/me", headers=bearer(fan["token"]))
|
||||||
|
assert r.status_code == 200
|
||||||
|
body = r.json()
|
||||||
|
assert body["total"] == 2
|
||||||
|
returned_ids = [item["id"] for item in body["items"]]
|
||||||
|
assert set(returned_ids) == set(ids)
|
||||||
|
|
||||||
|
assert _set_favorite(client, fan["token"], ids[0], False).status_code == 200
|
||||||
|
r = client.get("/api/v1/favorites/me", headers=bearer(fan["token"]))
|
||||||
|
assert r.status_code == 200
|
||||||
|
assert r.json()["total"] == 1
|
||||||
|
assert r.json()["items"][0]["id"] == ids[1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_favorite_missing_cloud(client: TestClient, monkeypatch) -> None:
|
||||||
|
user = register_and_confirm(
|
||||||
|
client,
|
||||||
|
monkeypatch,
|
||||||
|
f"missing-{uuid.uuid4().hex[:8]}@example.com",
|
||||||
|
"missing123",
|
||||||
|
"缺图用户",
|
||||||
|
)
|
||||||
|
r = _set_favorite(client, user["token"], str(uuid.uuid4()), True)
|
||||||
|
assert r.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_favorites_unauthenticated(client: TestClient) -> None:
|
||||||
|
r = client.get("/api/v1/favorites/me")
|
||||||
|
assert r.status_code == 401
|
||||||
Reference in New Issue
Block a user