mirror of
https://github.com/KingHWJ/WeReadBoard.git
synced 2026-11-03 18:24:58 +08:00
174 lines
6.7 KiB
Python
174 lines
6.7 KiB
Python
import re
|
|
import importlib
|
|
import os
|
|
import unittest
|
|
from datetime import timedelta
|
|
from unittest.mock import patch
|
|
|
|
from flask import Flask, jsonify
|
|
|
|
import auth
|
|
|
|
|
|
class AuthTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.app = Flask(__name__, template_folder="../templates")
|
|
self.app.config.update(
|
|
SECRET_KEY="test-secret",
|
|
AUTH_SETUP_TOKEN="one-time-token",
|
|
SESSION_COOKIE_SECURE=False,
|
|
)
|
|
self.accounts = {}
|
|
|
|
@self.app.get("/private")
|
|
def private_page():
|
|
return "private"
|
|
|
|
@self.app.get("/api/private")
|
|
def private_api():
|
|
return jsonify(ok=True)
|
|
|
|
self.create_patcher = patch.object(
|
|
auth, "create_account", side_effect=self.create_account
|
|
)
|
|
self.account_patcher = patch.object(
|
|
auth, "get_account", side_effect=lambda: self.accounts.get("account")
|
|
)
|
|
self.create_patcher.start()
|
|
self.account_patcher.start()
|
|
self.addCleanup(self.create_patcher.stop)
|
|
self.addCleanup(self.account_patcher.stop)
|
|
auth.register_auth(self.app)
|
|
self.app.config.update(
|
|
SECRET_KEY="test-secret",
|
|
AUTH_SETUP_TOKEN="one-time-token",
|
|
SESSION_COOKIE_SECURE=False,
|
|
)
|
|
self.client = self.app.test_client()
|
|
|
|
def create_account(self, username, password_hash):
|
|
if "account" in self.accounts:
|
|
return False
|
|
self.accounts["account"] = {
|
|
"username": username,
|
|
"password_hash": password_hash,
|
|
}
|
|
return True
|
|
|
|
@staticmethod
|
|
def csrf(response):
|
|
match = re.search(rb'name="csrf_token" value="([^"]+)"', response.data)
|
|
if not match:
|
|
raise AssertionError("login page did not contain a CSRF token")
|
|
return match.group(1).decode()
|
|
|
|
def test_anonymous_page_and_api_are_blocked(self):
|
|
page = self.client.get("/private")
|
|
api = self.client.get("/api/private")
|
|
self.assertEqual(page.status_code, 302)
|
|
self.assertEqual(page.location, "/login")
|
|
self.assertEqual(api.status_code, 401)
|
|
self.assertEqual(api.json["error"], "需要登录")
|
|
|
|
def test_current_and_legacy_reading_apis_are_blocked_by_real_app_guard(self):
|
|
with patch.dict(os.environ, {"WEREAD_SESSION_SECRET": "test-secret"}):
|
|
web_app = importlib.import_module("app").app
|
|
web_app.config.update(SECRET_KEY="test-secret", SESSION_COOKIE_SECURE=False)
|
|
with patch("app.pymysql.connect", side_effect=RuntimeError("test avoids database access")):
|
|
client = web_app.test_client()
|
|
self.assertEqual(client.get("/").status_code, 302)
|
|
self.assertEqual(client.get("/api/reading/status").status_code, 401)
|
|
self.assertEqual(client.get("/api/v2/status").status_code, 401)
|
|
|
|
def test_home_footer_displays_icp_record_link(self):
|
|
with patch.dict(os.environ, {"WEREAD_SESSION_SECRET": "test-secret"}):
|
|
web_app = importlib.import_module("app").app
|
|
web_app.config.update(SECRET_KEY="test-secret", SESSION_COOKIE_SECURE=False)
|
|
client = web_app.test_client()
|
|
with client.session_transaction() as session:
|
|
session["auth_user"] = "reader"
|
|
|
|
response = client.get("/")
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn(b"https://beian.miit.gov.cn/", response.data)
|
|
self.assertIn("浙ICP备202607916号-1".encode(), response.data)
|
|
self.assertIn("版本 v0.1".encode(), response.data)
|
|
|
|
def test_first_account_requires_setup_token_and_is_stored_as_hash(self):
|
|
page = self.client.get("/login")
|
|
csrf = self.csrf(page)
|
|
denied = self.client.post("/auth/initialize", data={
|
|
"csrf_token": csrf,
|
|
"setup_token": "wrong-token",
|
|
"username": "reader",
|
|
"password": "long-enough-password",
|
|
})
|
|
self.assertEqual(denied.status_code, 403)
|
|
self.assertNotIn("account", self.accounts)
|
|
|
|
created = self.client.post("/auth/initialize", data={
|
|
"csrf_token": csrf,
|
|
"setup_token": "one-time-token",
|
|
"username": "reader",
|
|
"password": "long-enough-password",
|
|
})
|
|
self.assertEqual(created.status_code, 302)
|
|
saved = self.accounts["account"]
|
|
self.assertEqual(saved["username"], "reader")
|
|
self.assertNotEqual(saved["password_hash"], "long-enough-password")
|
|
self.assertTrue(auth.check_password_hash(saved["password_hash"], "long-enough-password"))
|
|
with self.client.session_transaction() as session:
|
|
self.assertTrue(session.permanent)
|
|
self.assertEqual(session["auth_user"], "reader")
|
|
self.assertEqual(self.app.permanent_session_lifetime, timedelta(days=5))
|
|
|
|
def test_login_rejects_wrong_password_and_accepts_matching_hash(self):
|
|
saved_hash = auth.generate_password_hash("long-enough-password")
|
|
self.accounts["account"] = {"username": "阅读者", "password_hash": saved_hash}
|
|
page = self.client.get("/login")
|
|
csrf = self.csrf(page)
|
|
|
|
denied = self.client.post("/auth/login", data={
|
|
"csrf_token": csrf,
|
|
"username": "阅读者",
|
|
"password": "wrong-password",
|
|
})
|
|
self.assertEqual(denied.status_code, 401)
|
|
|
|
accepted = self.client.post("/auth/login", data={
|
|
"csrf_token": csrf,
|
|
"username": "阅读者",
|
|
"password": "long-enough-password",
|
|
})
|
|
self.assertEqual(accepted.status_code, 302)
|
|
self.assertEqual(self.client.get("/private").data, b"private")
|
|
|
|
def test_missing_session_secret_fails_closed(self):
|
|
self.app.config["SECRET_KEY"] = None
|
|
response = self.client.get("/login")
|
|
self.assertEqual(response.status_code, 503)
|
|
self.assertIn("服务未配置会话密钥".encode(), response.data)
|
|
|
|
def test_mutating_api_requires_csrf_token(self):
|
|
@self.app.post("/api/write")
|
|
def write_api():
|
|
return jsonify(ok=True)
|
|
|
|
self.client.get("/login")
|
|
self.accounts["account"] = {
|
|
"username": "reader",
|
|
"password_hash": auth.generate_password_hash("long-enough-password"),
|
|
}
|
|
# Simulate a session established through the regular authenticated login flow.
|
|
with self.client.session_transaction() as session:
|
|
session.permanent = True
|
|
session["auth_user"] = "reader"
|
|
session["csrf_token"] = "test-csrf"
|
|
|
|
self.assertEqual(self.client.post("/api/write").status_code, 400)
|
|
self.assertEqual(self.client.post("/api/write", headers={"X-CSRF-Token": "test-csrf"}).status_code, 200)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|