diff --git a/.idea/Django.iml b/.idea/Django.iml index f571432..07abf20 100644 --- a/.idea/Django.iml +++ b/.idea/Django.iml @@ -5,4 +5,8 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index db8786c..060d2c5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,4 @@ - - \ No newline at end of file diff --git a/doc/26组软件数据模型设计说明书.docx b/doc/26组软件数据模型设计说明书.docx deleted file mode 100644 index 66ccd68..0000000 Binary files a/doc/26组软件数据模型设计说明书.docx and /dev/null differ diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/context_processors.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/context_processors.py index cc2a5dc..4a62f34 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/context_processors.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/context_processors.py @@ -1,44 +1,88 @@ +# 导入日志模块,用于记录系统运行时的信息和错误 import logging +# 导入Django的时间工具,用于获取当前时间 from django.utils import timezone +# 导入自定义工具:缓存工具和获取博客设置的方法 from djangoblog.utils import cache, get_blog_setting +# 导入当前应用的模型:分类和文章模型 from .models import Category, Article +# 创建日志记录器,用于记录当前模块的日志信息 logger = logging.getLogger(__name__) def seo_processor(requests): + """ + 自定义上下文处理器:用于在所有模板中全局共享SEO相关的配置和数据 + 上下文处理器会在每次请求时被调用,返回的字典会自动注入到所有模板中 + + Args: + requests: HttpRequest对象,包含当前请求的信息(如协议、主机等) + + Returns: + dict: 包含网站配置、导航数据等的字典,供模板全局使用 + """ + # 定义缓存键,用于标识当前处理器的缓存数据 key = 'seo_processor' + # 尝试从缓存中获取数据,减少数据库查询和计算开销 value = cache.get(key) + + # 如果缓存中存在数据,直接返回缓存内容 if value: return value else: + # 缓存未命中时,记录日志并重新计算数据 logger.info('set processor cache.') + # 获取博客的全局设置(从数据库或其他配置源) setting = get_blog_setting() + + # 构建需要返回给模板的数据集 value = { + # 网站名称(用于页面标题等) 'SITE_NAME': setting.site_name, + # 是否显示谷歌广告 'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense, + # 谷歌广告代码 'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes, + # 网站SEO描述(用于meta标签) 'SITE_SEO_DESCRIPTION': setting.site_seo_description, + # 网站描述(用于页面展示) 'SITE_DESCRIPTION': setting.site_description, + # 网站关键词(用于meta标签,提升SEO) 'SITE_KEYWORDS': setting.site_keywords, + # 网站基础URL(协议+域名,如https://example.com/) 'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/', + # 文章摘要长度(用于列表页显示) 'ARTICLE_SUB_LENGTH': setting.article_sub_length, + # 导航栏显示的分类列表(从数据库查询所有分类) 'nav_category_list': Category.objects.all(), + # 导航栏显示的页面列表(筛选类型为"页面"且状态为"已发布"的文章) 'nav_pages': Article.objects.filter( - type='p', - status='p'), + type='p', # 'p'表示页面(page),区别于普通文章(article) + status='p'), # 'p'表示已发布(published) + # 是否开启网站评论功能 'OPEN_SITE_COMMENT': setting.open_site_comment, + # 网站备案号 'BEIAN_CODE': setting.beian_code, + # 网站统计代码(如百度统计、Google Analytics) 'ANALYTICS_CODE': setting.analytics_code, + # 公安备案号 "BEIAN_CODE_GONGAN": setting.gongan_beiancode, + # 是否显示公安备案号 "SHOW_GONGAN_CODE": setting.show_gongan_code, + # 当前年份(用于页脚版权信息等) "CURRENT_YEAR": timezone.now().year, + # 全局页头代码(如自定义CSS、JS) "GLOBAL_HEADER": setting.global_header, + # 全局页脚代码 "GLOBAL_FOOTER": setting.global_footer, + # 评论是否需要审核后才显示 "COMMENT_NEED_REVIEW": setting.comment_need_review, } - cache.set(key, value, 60 * 60 * 10) - return value + # 将数据存入缓存,有效期为10小时(60秒*60分*10小时) + # 减少重复计算和数据库查询,提升性能 + cache.set(key, value, 60 * 60 * 10) + return value \ No newline at end of file diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/search_indexes.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/search_indexes.py index 8ebb26a..7f1dfac 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/search_indexes.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/search_indexes.py @@ -1,26 +1,13 @@ -# 从haystack框架导入索引相关的模块,用于实现全文搜索功能 from haystack import indexes -# 导入当前项目中blog应用的Article模型,该模型对应需要被索引的文章数据 from blog.models import Article -# 定义ArticleIndex类,继承自SearchIndex和Indexable,用于配置Article模型的搜索索引 -# SearchIndex:提供索引的核心功能,定义了如何从模型中提取数据构建索引 -# Indexable:标识该类可被索引,要求实现get_model方法来指定关联的模型 class ArticleIndex(indexes.SearchIndex, indexes.Indexable): - # 定义一个text字段作为文档的主要索引字段(document=True表示这是主要搜索字段) - # use_template=True表示使用模板来定义该字段需要索引的内容(通常在templates/search/indexes/blog/article_text.txt中配置) - # 该字段会聚合模型中需要被搜索的字段(如标题、正文等),作为全文搜索的基础 text = indexes.CharField(document=True, use_template=True) - # 实现Indexable接口的方法,返回当前索引关联的模型类 - # 作用:告诉haystack该索引对应的数据来自哪个模型 def get_model(self): return Article - # 定义需要被索引的查询集(即哪些数据会被纳入搜索范围) - # using参数用于指定搜索引擎(多引擎场景下使用),默认None表示使用默认引擎 - # 这里返回状态为'p'(假设表示"已发布")的文章,确保只有已发布的内容可被搜索 def index_queryset(self, using=None): - return self.get_model().objects.filter(status='p') \ No newline at end of file + return self.get_model().objects.filter(status='p') diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py index e1c78e4..adf2703 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/blog/urls.py @@ -1,108 +1,62 @@ -# 导入 Django 内置的路径配置工具和缓存装饰器 from django.urls import path from django.views.decorators.cache import cache_page -# 导入当前应用(blog)的视图模块,用于关联路由与视图逻辑 from . import views -# 定义应用命名空间(namespace),用于在模板或反向解析时区分不同应用的路由 -# 例如:在模板中使用 {% url 'blog:index' %} 生成首页链接 app_name = "blog" - -# 路由配置列表,每个 path 对应一个 URL 规则与视图的映射 urlpatterns = [ - # 首页路由:匹配根路径(网站域名/) path( - r'', # URL 路径表达式,空字符串表示根路径 - views.IndexView.as_view(), # 关联的视图类(IndexView),通过 as_view() 转换为可调用视图 - name='index' # 路由名称,用于反向解析(如 reverse('blog:index')) - ), - - # 分页首页路由:匹配带页码的首页(如 /page/2/) + r'', + views.IndexView.as_view(), + name='index'), path( - r'page//', # 是路径参数,int 表示接收整数类型,page 是参数名 - views.IndexView.as_view(), # 复用首页视图类,视图中会通过 page 参数处理分页 - name='index_page' - ), - - # 文章详情页路由:按日期和文章ID匹配(如 /article/2023/10/20/100.html) + r'page//', + views.IndexView.as_view(), + name='index_page'), path( r'article////.html', - # 路径参数:year(年)、month(月)、day(日)、article_id(文章ID),均为整数 - views.ArticleDetailView.as_view(), # 文章详情视图类,处理文章展示逻辑 - name='detailbyid' - ), - - # 分类详情页路由:按分类名匹配(如 /category/tech.html) + views.ArticleDetailView.as_view(), + name='detailbyid'), path( r'category/.html', - # :slug 类型表示接收字母、数字、下划线和连字符组成的字符串(适合URL友好的名称) - views.CategoryDetailView.as_view(), # 分类详情视图类,展示该分类下的文章 - name='category_detail' - ), - - # 分类详情分页路由:带页码的分类页(如 /category/tech/2.html) + views.CategoryDetailView.as_view(), + name='category_detail'), path( r'category//.html', - views.CategoryDetailView.as_view(), # 复用分类视图类,通过 page 参数分页 - name='category_detail_page' - ), - - # 作者详情页路由:按作者名匹配(如 /author/alice.html) + views.CategoryDetailView.as_view(), + name='category_detail_page'), path( r'author/.html', - # :未指定类型,默认接收字符串(除特殊字符外) - views.AuthorDetailView.as_view(), # 作者详情视图类,展示该作者的文章 - name='author_detail' - ), - - # 作者详情分页路由:带页码的作者页(如 /author/alice/2.html) + views.AuthorDetailView.as_view(), + name='author_detail'), path( r'author//.html', - views.AuthorDetailView.as_view(), # 复用作者视图类,通过 page 参数分页 - name='author_detail_page' - ), - - # 标签详情页路由:按标签名匹配(如 /tag/python.html) + views.AuthorDetailView.as_view(), + name='author_detail_page'), path( r'tag/.html', - views.TagDetailView.as_view(), # 标签详情视图类,展示该标签下的文章 - name='tag_detail' - ), - - # 标签详情分页路由:带页码的标签页(如 /tag/python/2.html) + views.TagDetailView.as_view(), + name='tag_detail'), path( r'tag//.html', - views.TagDetailView.as_view(), # 复用标签视图类,通过 page 参数分页 - name='tag_detail_page' - ), - - # 归档页路由:匹配 /archives.html + views.TagDetailView.as_view(), + name='tag_detail_page'), path( 'archives.html', - # 缓存装饰器:cache_page(60*60) 表示缓存该页面1小时(60秒*60),减轻服务器压力 - cache_page(60 * 60)(views.ArchivesView.as_view()), - name='archives' # 归档视图,通常展示按日期分组的文章列表 - ), - - # 友情链接页路由:匹配 /links.html + cache_page( + 60 * 60)( + views.ArchivesView.as_view()), + name='archives'), path( 'links.html', - views.LinkListView.as_view(), # 友情链接视图类,展示网站链接列表 - name='links' - ), - - # 文件上传路由:匹配 /upload + views.LinkListView.as_view(), + name='links'), path( r'upload', - views.fileupload, # 关联函数视图(非类视图),处理文件上传逻辑 - name='upload' - ), - - # 缓存清理路由:匹配 /clean + views.fileupload, + name='upload'), path( r'clean', - views.clean_cache_view, # 关联缓存清理视图,用于手动触发缓存清理 - name='clean' - ), -] \ No newline at end of file + views.clean_cache_view, + name='clean'), +] diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/comments/models.py b/src/DjangoBlog-master(1)/DjangoBlog-master/comments/models.py index 0d052b1..7c3bbc8 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/comments/models.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/comments/models.py @@ -36,4 +36,4 @@ class Comment(models.Model): get_latest_by = 'id' def __str__(self): - return self.body \ No newline at end of file + return self.body diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/tests.py b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/tests.py index 01237d9..c2e16a7 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/tests.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/djangoblog/tests.py @@ -1,15 +1,19 @@ from django.test import TestCase from djangoblog.utils import * - - +# 定义测试类,继承自TestCase,用于测试Django博客项目中的工具类/函数 class DjangoBlogTest(TestCase): + # 本测试用例无需前置初始化操作,故保持空实现 def setUp(self): pass + # 1. 测试SHA256加密工具函数get_sha256 + # 对字符串'test'进行SHA256加密,获取加密结果 def test_utils(self): md5 = get_sha256('test') self.assertIsNotNone(md5) + # 2. 测试Markdown解析工具类CommonMarkdown + # 调用get_markdown方法,解析一段包含多种元素的Markdown文本 c = CommonMarkdown.get_markdown(''' # Title1 diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/admin.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/admin.py index d3ffcd6..57eab5f 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/admin.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/admin.py @@ -5,93 +5,50 @@ from django.contrib import admin from django.urls import reverse from django.utils.html import format_html -# 获取当前模块的日志记录器 logger = logging.getLogger(__name__) class OAuthUserAdmin(admin.ModelAdmin): - """ - OAuth用户管理后台配置类 - 用于自定义Django Admin中OAuth用户模型的显示和行为 - """ - - # 搜索字段配置,支持按昵称和邮箱搜索 search_fields = ('nickname', 'email') - - # 每页显示的项目数量 list_per_page = 20 - - # 列表页显示的字段 list_display = ( 'id', 'nickname', - 'link_to_usermodel', # 自定义方法:链接到用户模型 - 'show_user_image', # 自定义方法:显示用户头像 - 'type', # OAuth类型 + 'link_to_usermodel', + 'show_user_image', + 'type', 'email', ) - - # 可点击进入编辑页的字段 list_display_links = ('id', 'nickname') - - # 右侧筛选器配置 list_filter = ('author', 'type',) - - # 只读字段列表,开始为空 readonly_fields = [] def get_readonly_fields(self, request, obj=None): - """ - 动态获取只读字段 - 将所有模型字段和多对多字段设为只读 - """ return list(self.readonly_fields) + \ - [field.name for field in obj._meta.fields] + \ - [field.name for field in obj._meta.many_to_many] + [field.name for field in obj._meta.fields] + \ + [field.name for field in obj._meta.many_to_many] def has_add_permission(self, request): - """ - 禁用添加权限 - OAuth用户应该通过OAuth流程自动创建,而不是手动添加 - """ return False def link_to_usermodel(self, obj): - """ - 自定义方法:生成指向关联用户模型的链接 - """ if obj.author: - # 获取用户模型的app和模型名称信息 info = (obj.author._meta.app_label, obj.author._meta.model_name) - # 生成用户编辑页面的URL link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,)) - # 返回HTML格式的链接 return format_html( u'%s' % (link, obj.author.nickname if obj.author.nickname else obj.author.email)) def show_user_image(self, obj): - """ - 自定义方法:在管理后台显示用户头像 - """ - img = obj.picture # 获取头像图片URL + img = obj.picture return format_html( u'' % (img)) - # 设置自定义方法在管理后台的显示名称 link_to_usermodel.short_description = '用户' show_user_image.short_description = '用户头像' class OAuthConfigAdmin(admin.ModelAdmin): - """ - OAuth配置管理后台配置类 - 用于管理不同OAuth服务的配置信息 - """ - - # 列表页显示的字段 list_display = ('type', 'appkey', 'appsecret', 'is_enable') - - # 右侧筛选器配置 list_filter = ('type',) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/apps.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/apps.py index 42788ab..17fcea2 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/apps.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/apps.py @@ -2,14 +2,4 @@ from django.apps import AppConfig class OauthConfig(AppConfig): - """ - OAuth应用配置类 - 用于配置Django中OAuth应用的元数据和启动行为 - - 这个类继承自Django的AppConfig基类,用于定义 - OAuth应用在Django项目中的配置信息 - """ - - # 指定应用的Python路径,Django使用这个名称来识别应用 - # 这应该与应用目录的名称保持相同 - name = 'oauth' \ No newline at end of file + name = 'oauth' diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/forms.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/forms.py index 0db6327..0e4ede3 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/forms.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/forms.py @@ -3,32 +3,10 @@ from django.forms import widgets class RequireEmailForm(forms.Form): - """ - 邮箱验证表单类 - 用于在OAuth登录过程中要求用户提供邮箱地址 - 通常在第三方OAuth服务没有返回邮箱信息时使用 - """ - - # 邮箱字段,标签显示为'电子邮箱' email = forms.EmailField(label='电子邮箱', required=True) - - # OAuth用户ID隐藏字段,用于关联OAuth用户记录 oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False) def __init__(self, *args, **kwargs): - """ - 初始化方法,自定义表单字段的显示属性 - - Args: - *args: 位置参数 - **kwargs: 关键字参数 - """ - # 调用父类的初始化方法 super(RequireEmailForm, self).__init__(*args, **kwargs) - - # 自定义邮箱字段的输入控件,添加占位符和CSS类 self.fields['email'].widget = widgets.EmailInput( - attrs={ - 'placeholder': "email", # 输入框内的提示文字 - "class": "form-control" # Bootstrap样式类 - }) \ No newline at end of file + attrs={'placeholder': "email", "class": "form-control"}) diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/models.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/models.py index b4b570d..be838ed 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/models.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/models.py @@ -7,113 +7,61 @@ from django.utils.translation import gettext_lazy as _ class OAuthUser(models.Model): - """ - OAuth用户模型 - 用于存储通过第三方OAuth服务登录的用户信息 - """ - - # 关联到系统的本地用户,允许为空 author = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_('author'), blank=True, null=True, - on_delete=models.CASCADE) # 删除用户时级联删除OAuth记录 - - # OAuth服务提供的用户唯一标识 + on_delete=models.CASCADE) openid = models.CharField(max_length=50) - - # 用户在第三方平台的昵称 nickname = models.CharField(max_length=50, verbose_name=_('nick name')) - - # OAuth访问令牌,用于后续API调用 token = models.CharField(max_length=150, null=True, blank=True) - - # 用户在第三方平台的头像URL picture = models.CharField(max_length=350, blank=True, null=True) - - # OAuth服务类型(如github、weibo等) type = models.CharField(blank=False, null=False, max_length=50) - - # 用户在第三方平台的邮箱 email = models.CharField(max_length=50, null=True, blank=True) - - # 存储额外的OAuth返回数据(JSON格式) metadata = models.TextField(null=True, blank=True) - - # 记录创建时间 creation_time = models.DateTimeField(_('creation time'), default=now) - - # 最后修改时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) def __str__(self): - """返回对象的字符串表示,显示用户昵称""" return self.nickname class Meta: - """模型元数据配置""" - verbose_name = _('oauth user') # 单数名称 - verbose_name_plural = verbose_name # 复数名称 - ordering = ['-creation_time'] # 按创建时间降序排列 + verbose_name = _('oauth user') + verbose_name_plural = verbose_name + ordering = ['-creation_time'] class OAuthConfig(models.Model): - """ - OAuth服务配置模型 - 用于存储不同OAuth服务的应用配置信息 - """ - - # OAuth服务类型选择项 TYPE = ( - ('weibo', _('weibo')), # 微博 - ('google', _('google')), # 谷歌 - ('github', 'GitHub'), # GitHub - ('facebook', 'FaceBook'), # Facebook - ('qq', 'QQ'), # QQ + ('weibo', _('weibo')), + ('google', _('google')), + ('github', 'GitHub'), + ('facebook', 'FaceBook'), + ('qq', 'QQ'), ) - - # OAuth服务类型,从预定义选项中选择 type = models.CharField(_('type'), max_length=10, choices=TYPE, default='a') - - # OAuth应用的AppKey/Client ID appkey = models.CharField(max_length=200, verbose_name='AppKey') - - # OAuth应用的AppSecret/Client Secret appsecret = models.CharField(max_length=200, verbose_name='AppSecret') - - # OAuth回调URL callback_url = models.CharField( max_length=200, verbose_name=_('callback url'), blank=False, default='') - - # 是否启用该OAuth服务 is_enable = models.BooleanField( _('is enable'), default=True, blank=False, null=False) - - # 配置创建时间 creation_time = models.DateTimeField(_('creation time'), default=now) - - # 配置最后修改时间 last_modify_time = models.DateTimeField(_('last modify time'), default=now) def clean(self): - """ - 数据验证方法 - 确保同类型的OAuth配置只能有一个 - """ if OAuthConfig.objects.filter( type=self.type).exclude(id=self.id).count(): raise ValidationError(_(self.type + _('already exists'))) def __str__(self): - """返回对象的字符串表示,显示OAuth服务类型""" return self.type class Meta: - """模型元数据配置""" - verbose_name = 'oauth配置' # 单数名称(中文) - verbose_name_plural = verbose_name # 复数名称 - ordering = ['-creation_time'] # 按创建时间降序排列 \ No newline at end of file + verbose_name = 'oauth配置' + verbose_name_plural = verbose_name + ordering = ['-creation_time'] diff --git a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/tests.py b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/tests.py index 8198f84..bb23b9b 100644 --- a/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/tests.py +++ b/src/DjangoBlog-master(1)/DjangoBlog-master/oauth/tests.py @@ -13,81 +13,43 @@ from oauth.oauthmanager import BaseOauthManager # Create your tests here. class OAuthConfigTest(TestCase): - """ - OAuth配置基础测试类 - 测试OAuth登录流程的基本功能 - """ - def setUp(self): - """ - 测试初始化方法,在每个测试方法执行前运行 - """ - self.client = Client() # Django测试客户端,用于模拟HTTP请求 - self.factory = RequestFactory() # 用于创建请求对象的工厂 + self.client = Client() + self.factory = RequestFactory() def test_oauth_login_test(self): - """ - 测试OAuth登录流程 - 验证微博OAuth登录的跳转和授权流程 - """ - # 创建微博OAuth配置 c = OAuthConfig() c.type = 'weibo' c.appkey = 'appkey' c.appsecret = 'appsecret' c.save() - # 测试OAuth登录请求,应该重定向到微博授权页面 response = self.client.get('/oauth/oauthlogin?type=weibo') - self.assertEqual(response.status_code, 302) # 验证重定向状态码 - self.assertTrue("api.weibo.com" in response.url) # 验证跳转到微博授权页面 + self.assertEqual(response.status_code, 302) + self.assertTrue("api.weibo.com" in response.url) - # 测试授权回调处理,应该重定向到首页 response = self.client.get('/oauth/authorize?type=weibo&code=code') - self.assertEqual(response.status_code, 302) # 验证重定向状态码 - self.assertEqual(response.url, '/') # 验证跳转到首页 + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, '/') class OauthLoginTest(TestCase): - """ - OAuth登录详细测试类 - 测试各种OAuth服务提供商的登录流程 - """ - def setUp(self) -> None: - """ - 测试初始化方法 - """ - self.client = Client() # Django测试客户端 - self.factory = RequestFactory() # 请求工厂 - self.apps = self.init_apps() # 初始化所有OAuth应用配置 + self.client = Client() + self.factory = RequestFactory() + self.apps = self.init_apps() def init_apps(self): - """ - 初始化所有支持的OAuth应用配置 - 为每种OAuth服务创建测试配置 - """ - # 获取所有OAuth管理器的子类实例 applications = [p() for p in BaseOauthManager.__subclasses__()] for application in applications: - # 为每个OAuth服务创建配置 c = OAuthConfig() - c.type = application.ICON_NAME.lower() # 服务类型(小写) - c.appkey = 'appkey' # 测试用的AppKey - c.appsecret = 'appsecret' # 测试用的AppSecret + c.type = application.ICON_NAME.lower() + c.appkey = 'appkey' + c.appsecret = 'appsecret' c.save() return applications def get_app_by_type(self, type): - """ - 根据类型获取对应的OAuth应用实例 - - Args: - type: OAuth服务类型 - - Returns: - 对应的OAuth管理器实例 - """ for app in self.apps: if app.ICON_NAME.lower() == type: return app @@ -95,117 +57,73 @@ class OauthLoginTest(TestCase): @patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_get") def test_weibo_login(self, mock_do_get, mock_do_post): - """ - 测试微博OAuth登录流程 - 使用mock模拟API调用 - """ weibo_app = self.get_app_by_type('weibo') - assert weibo_app # 确保获取到微博应用实例 - - # 获取授权URL + assert weibo_app url = weibo_app.get_authorization_url() - - # 设置mock返回值 - 获取access token mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" }) - # 设置mock返回值 - 获取用户信息 mock_do_get.return_value = json.dumps({ "avatar_large": "avatar_large", "screen_name": "screen_name", "id": "id", "email": "email", }) - - # 执行获取access token的操作 userinfo = weibo_app.get_access_token_by_code('code') - - # 验证返回的用户信息 self.assertEqual(userinfo.token, 'access_token') self.assertEqual(userinfo.openid, 'id') @patch("oauth.oauthmanager.GoogleOauthManager.do_post") @patch("oauth.oauthmanager.GoogleOauthManager.do_get") def test_google_login(self, mock_do_get, mock_do_post): - """ - 测试Google OAuth登录流程 - """ google_app = self.get_app_by_type('google') assert google_app - url = google_app.get_authorization_url() - - # 模拟Google OAuth的token响应 mock_do_post.return_value = json.dumps({ "access_token": "access_token", "id_token": "id_token", }) - - # 模拟Google用户信息响应 mock_do_get.return_value = json.dumps({ "picture": "picture", "name": "name", - "sub": "sub", # Google的用户ID字段 + "sub": "sub", "email": "email", }) - token = google_app.get_access_token_by_code('code') userinfo = google_app.get_oauth_userinfo() - - # 验证用户信息 self.assertEqual(userinfo.token, 'access_token') - self.assertEqual(userinfo.openid, 'sub') # Google使用sub作为用户ID + self.assertEqual(userinfo.openid, 'sub') @patch("oauth.oauthmanager.GitHubOauthManager.do_post") @patch("oauth.oauthmanager.GitHubOauthManager.do_get") def test_github_login(self, mock_do_get, mock_do_post): - """ - 测试GitHub OAuth登录流程 - """ github_app = self.get_app_by_type('github') assert github_app - url = github_app.get_authorization_url() - # 验证GitHub授权URL包含必要信息 self.assertTrue("github.com" in url) self.assertTrue("client_id" in url) - - # 模拟GitHub的token响应(字符串格式) mock_do_post.return_value = "access_token=gho_16C7e42F292c6912E7710c838347Ae178B4a&scope=repo%2Cgist&token_type=bearer" - - # 模拟GitHub用户信息响应 mock_do_get.return_value = json.dumps({ "avatar_url": "avatar_url", "name": "name", "id": "id", "email": "email", }) - token = github_app.get_access_token_by_code('code') userinfo = github_app.get_oauth_userinfo() - - # 验证用户信息 self.assertEqual(userinfo.token, 'gho_16C7e42F292c6912E7710c838347Ae178B4a') self.assertEqual(userinfo.openid, 'id') @patch("oauth.oauthmanager.FaceBookOauthManager.do_post") @patch("oauth.oauthmanager.FaceBookOauthManager.do_get") def test_facebook_login(self, mock_do_get, mock_do_post): - """ - 测试Facebook OAuth登录流程 - """ facebook_app = self.get_app_by_type('facebook') assert facebook_app - url = facebook_app.get_authorization_url() - self.assertTrue("facebook.com" in url) # 验证Facebook授权URL - - # 模拟Facebook token响应 + self.assertTrue("facebook.com" in url) mock_do_post.return_value = json.dumps({ "access_token": "access_token", }) - - # 模拟Facebook用户信息响应(嵌套结构) mock_do_get.return_value = json.dumps({ "name": "name", "id": "id", @@ -216,16 +134,14 @@ class OauthLoginTest(TestCase): } } }) - token = facebook_app.get_access_token_by_code('code') userinfo = facebook_app.get_oauth_userinfo() - self.assertEqual(userinfo.token, 'access_token') @patch("oauth.oauthmanager.QQOauthManager.do_get", side_effect=[ - 'access_token=access_token&expires_in=3600', # 第一次调用:获取token - 'callback({"client_id":"appid","openid":"openid"} );', # 第二次调用:获取openid - json.dumps({ # 第三次调用:获取用户信息 + 'access_token=access_token&expires_in=3600', + 'callback({"client_id":"appid","openid":"openid"} );', + json.dumps({ "nickname": "nickname", "email": "email", "figureurl": "figureurl", @@ -233,33 +149,21 @@ class OauthLoginTest(TestCase): }) ]) def test_qq_login(self, mock_do_get): - """ - 测试QQ OAuth登录流程 - 使用side_effect模拟多次不同的API响应 - """ qq_app = self.get_app_by_type('qq') assert qq_app - url = qq_app.get_authorization_url() - self.assertTrue("qq.com" in url) # 验证QQ授权URL - + self.assertTrue("qq.com" in url) token = qq_app.get_access_token_by_code('code') userinfo = qq_app.get_oauth_userinfo() - self.assertEqual(userinfo.token, 'access_token') @patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_get") def test_weibo_authoriz_login_with_email(self, mock_do_get, mock_do_post): - """ - 测试包含邮箱的微博授权登录完整流程 - 验证用户认证和会话管理 - """ - # 模拟获取access token + mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" }) - # 模拟用户信息(包含邮箱) mock_user_info = { "avatar_large": "avatar_large", "screen_name": "screen_name1", @@ -268,31 +172,25 @@ class OauthLoginTest(TestCase): } mock_do_get.return_value = json.dumps(mock_user_info) - # 测试登录跳转 response = self.client.get('/oauth/oauthlogin?type=weibo') self.assertEqual(response.status_code, 302) self.assertTrue("api.weibo.com" in response.url) - # 测试授权回调 response = self.client.get('/oauth/authorize?type=weibo&code=code') self.assertEqual(response.status_code, 302) self.assertEqual(response.url, '/') - # 验证用户认证状态 user = auth.get_user(self.client) assert user.is_authenticated self.assertTrue(user.is_authenticated) self.assertEqual(user.username, mock_user_info['screen_name']) self.assertEqual(user.email, mock_user_info['email']) - - # 登出后再次测试 self.client.logout() response = self.client.get('/oauth/authorize?type=weibo&code=code') self.assertEqual(response.status_code, 302) self.assertEqual(response.url, '/') - # 再次验证用户认证状态 user = auth.get_user(self.client) assert user.is_authenticated self.assertTrue(user.is_authenticated) @@ -302,15 +200,10 @@ class OauthLoginTest(TestCase): @patch("oauth.oauthmanager.WBOauthManager.do_post") @patch("oauth.oauthmanager.WBOauthManager.do_get") def test_weibo_authoriz_login_without_email(self, mock_do_get, mock_do_post): - """ - 测试不包含邮箱的微博授权登录流程 - 验证邮箱补充流程 - """ - # 模拟获取access token + mock_do_post.return_value = json.dumps({"access_token": "access_token", "uid": "uid" }) - # 模拟用户信息(不包含邮箱) mock_user_info = { "avatar_large": "avatar_large", "screen_name": "screen_name1", @@ -318,34 +211,28 @@ class OauthLoginTest(TestCase): } mock_do_get.return_value = json.dumps(mock_user_info) - # 测试登录跳转 response = self.client.get('/oauth/oauthlogin?type=weibo') self.assertEqual(response.status_code, 302) self.assertTrue("api.weibo.com" in response.url) - # 测试授权回调 - 应该重定向到邮箱补充页面 response = self.client.get('/oauth/authorize?type=weibo&code=code') + self.assertEqual(response.status_code, 302) - # 解析OAuth用户ID oauth_user_id = int(response.url.split('/')[-1].split('.')[0]) self.assertEqual(response.url, f'/oauth/requireemail/{oauth_user_id}.html') - # 提交邮箱信息 response = self.client.post(response.url, {'email': 'test@gmail.com', 'oauthid': oauth_user_id}) - self.assertEqual(response.status_code, 302) - # 生成邮箱验证签名 + self.assertEqual(response.status_code, 302) sign = get_sha256(settings.SECRET_KEY + str(oauth_user_id) + settings.SECRET_KEY) - # 验证绑定成功URL url = reverse('oauth:bindsuccess', kwargs={ 'oauthid': oauth_user_id, }) self.assertEqual(response.url, f'{url}?type=email') - # 验证邮箱确认流程 path = reverse('oauth:email_confirm', kwargs={ 'id': oauth_user_id, 'sign': sign @@ -353,12 +240,10 @@ class OauthLoginTest(TestCase): response = self.client.get(path) self.assertEqual(response.status_code, 302) self.assertEqual(response.url, f'/oauth/bindsuccess/{oauth_user_id}.html?type=success') - - # 验证最终用户状态 user = auth.get_user(self.client) from oauth.models import OAuthUser oauth_user = OAuthUser.objects.get(author=user) self.assertTrue(user.is_authenticated) self.assertEqual(user.username, mock_user_info['screen_name']) self.assertEqual(user.email, 'test@gmail.com') - self.assertEqual(oauth_user.pk, oauth_user_id) \ No newline at end of file + self.assertEqual(oauth_user.pk, oauth_user_id)