Skip to content

Commit dc3168d

Browse files
authored
#540 서버 오류 응답 상태 코드 수정 (#711)
1 parent d8b8568 commit dc3168d

7 files changed

Lines changed: 117 additions & 23 deletions

File tree

backend/account/decorators.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,19 @@ def __init__(self, func):
1818
def __get__(self, obj, obj_type):
1919
return functools.partial(self.__call__, obj)
2020

21-
def error(self, data):
22-
return JSONResponse.response({"error": "permission-denied", "data": data})
21+
def error(self, data, status=403):
22+
return JSONResponse.response({"error": "permission-denied", "data": data}, status=status)
2323

2424
def __call__(self, *args, **kwargs):
2525
self.request = args[1]
2626

2727
if self.check_permission():
2828
if self.request.user.is_disabled:
29-
return self.error("Your account is disabled")
29+
return self.error("Your account is disabled", status=403)
3030
return self.func(*args, **kwargs)
3131
else:
32-
return self.error("Please login first")
32+
status = 401 if not self.request.user.is_authenticated else 403
33+
return self.error("Please login first", status=status)
3334

3435
def check_permission(self):
3536
raise NotImplementedError()
@@ -212,7 +213,7 @@ def error(self, data):
212213
return JSONResponse.response({
213214
"error": "permission-denied",
214215
"data": "Scheduler token is invalid or missing",
215-
})
216+
}, status=403)
216217

217218

218219
def scheduler_only(view_func):

backend/account/middleware.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,11 @@ def process_request(self, request):
134134
path = request.path_info
135135
if path.startswith("/admin/") or path.startswith("/api/admin/"):
136136
if not (request.user.is_authenticated and request.user.is_admin_role()):
137-
return JSONResponse.response({"error": "login-required", "data": "Please login in first"})
137+
status = 401 if not request.user.is_authenticated else 403
138+
return JSONResponse.response(
139+
{"error": "login-required", "data": "Please login in first"},
140+
status=status,
141+
)
138142

139143

140144
class LogSqlMiddleware(MiddlewareMixin):

backend/account/tests.py

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
from options.options import SysOptions
1818

1919
from .models import AdminType, ProblemPermission, User
20-
from .decorators import scheduler_only
21-
from .middleware import RequestIDMiddleware
20+
from .decorators import login_required, scheduler_only
21+
from .middleware import AdminRoleRequiredMiddleware, RequestIDMiddleware
2222
from .tasks import calculate_user_score_basis, calculate_user_score_fluctuation
2323

2424

@@ -65,6 +65,35 @@ def test_generates_request_id_when_header_has_no_safe_characters(self):
6565
self.assertEqual(response["X-Request-ID"], request.request_id)
6666

6767

68+
class AdminRoleRequiredMiddlewareTest(SimpleTestCase):
69+
70+
def setUp(self):
71+
self.factory = RequestFactory()
72+
73+
def test_admin_api_requires_login_with_http_401(self):
74+
middleware = AdminRoleRequiredMiddleware(lambda request: JsonResponse({"ok": True}))
75+
request = self.factory.get("/api/admin/problem")
76+
request.user = mock.MagicMock()
77+
request.user.is_authenticated = False
78+
79+
response = middleware(request)
80+
81+
self.assertEqual(response.status_code, 401)
82+
self.assertEqual(response.data, {"error": "login-required", "data": "Please login in first"})
83+
84+
def test_admin_api_requires_admin_role_with_http_403(self):
85+
middleware = AdminRoleRequiredMiddleware(lambda request: JsonResponse({"ok": True}))
86+
request = self.factory.get("/api/admin/problem")
87+
request.user = mock.MagicMock()
88+
request.user.is_authenticated = True
89+
request.user.is_admin_role.return_value = False
90+
91+
response = middleware(request)
92+
93+
self.assertEqual(response.status_code, 403)
94+
self.assertEqual(response.data, {"error": "login-required", "data": "Please login in first"})
95+
96+
6897
class PermissionDecoratorTest(APITestCase):
6998
"""
7099
데코레이터 테스트
@@ -78,7 +107,18 @@ def setUp(self):
78107
self.request.user.is_authenticated = mock.MagicMock()
79108

80109
def test_login_required(self):
81-
self.request.user.is_authenticated.return_value = False
110+
class TestAPIView(APIView):
111+
112+
@login_required
113+
def get(self, request):
114+
return self.success("Success")
115+
116+
self.request.user.is_authenticated = False
117+
118+
response = TestAPIView().get(self.request)
119+
120+
self.assertEqual(response.status_code, 401)
121+
self.assertFailed(response, "Please login first")
82122

83123
def test_admin_required(self):
84124
pass
@@ -106,13 +146,15 @@ def test_empty_environment_token(self):
106146
""" Test when SCHEDULER_TOKEN is not set in the environment."""
107147
request = self.factory.post("/", HTTP_X_SCHEDULER_TOKEN='secret')
108148
response = self.view.post(request)
149+
self.assertEqual(response.status_code, 403)
109150
self.assertFailed(response)
110151

111152
def test_missing_header_token(self):
112153
""" Test when SCHEDULER_TOKEN is not set in the environment and request header is missing."""
113154
with mock.patch('os.environ', {}):
114155
request = self.factory.post("/")
115156
response = self.view.post(request)
157+
self.assertEqual(response.status_code, 403)
116158
self.assertFailed(response)
117159

118160
def test_valid_token(self):
@@ -127,6 +169,7 @@ def test_invalid_token(self):
127169
with mock.patch('os.environ', {'SCHEDULER_TOKEN': 'secret'}):
128170
request = self.factory.post("/", HTTP_X_SCHEDULER_TOKEN='wrong_secret')
129171
response = self.view.post(request)
172+
self.assertEqual(response.status_code, 403)
130173
self.assertFailed(response)
131174

132175

backend/utils/api/api.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ class JSONResponse(object):
6161
content_type = ContentType.json_response
6262

6363
@classmethod
64-
def response(cls, data):
65-
resp = HttpResponse(json.dumps(data, indent=4), content_type=cls.content_type)
64+
def response(cls, data, status=200):
65+
resp = HttpResponse(json.dumps(data, indent=4), content_type=cls.content_type, status=status)
6666
resp.data = data
6767
return resp
6868

@@ -116,14 +116,14 @@ def _get_request_data(self, request):
116116

117117
return request.data
118118

119-
def response(self, data):
120-
return self.response_class.response(data)
119+
def response(self, data, status=200):
120+
return self.response_class.response(data, status=status)
121121

122122
def success(self, data=None):
123123
return self.response({"error": None, "data": data})
124124

125-
def error(self, msg="error", err="error"):
126-
return self.response({"error": err, "data": msg})
125+
def error(self, msg="error", err="error", status=400):
126+
return self.response({"error": err, "data": msg}, status=status)
127127

128128
def extract_errors(self, errors, key="field"):
129129
if isinstance(errors, dict):
@@ -145,7 +145,7 @@ def invalid_serializer(self, serializer):
145145
return self.error(err=f"invalid-{key}", msg=msg)
146146

147147
def server_error(self):
148-
return self.error(err="server-error", msg="server error")
148+
return self.error(err="server-error", msg="server error", status=500)
149149

150150
def paginate_data(self, request, query_set, object_serializer=None):
151151
"""

backend/utils/tests.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
import os
44
import pickle
55
from unittest.mock import patch, mock_open, MagicMock
6-
from django.test import SimpleTestCase, TestCase, override_settings
6+
from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings
77
from django.core.cache import cache
88
from prometheus_client.core import GaugeMetricFamily
99

1010
from utils.json_logging import CodePlaceJsonFormatter
11+
from utils.api import APIView
1112
from utils.observability_metrics import CodePlaceCollector
1213
from utils import observability_tracing
1314
from utils.testcase_cache import TestCaseCacheManager
@@ -33,6 +34,36 @@ def test_reads_pickled_byte_values_from_django_redis_hash(self):
3334
self.assertEqual(bucket._last_capacity, 7.0)
3435

3536

37+
class APIViewStatusCodeTest(SimpleTestCase):
38+
39+
def setUp(self):
40+
self.factory = RequestFactory()
41+
42+
def test_server_exception_returns_http_500_with_existing_json_body(self):
43+
44+
class BrokenAPIView(APIView):
45+
46+
def get(self, request):
47+
raise RuntimeError("boom")
48+
49+
response = BrokenAPIView.as_view()(self.factory.get("/"))
50+
51+
self.assertEqual(response.status_code, 500)
52+
self.assertEqual(response.data, {"error": "server-error", "data": "server error"})
53+
54+
def test_business_error_returns_http_400(self):
55+
56+
class BusinessErrorAPIView(APIView):
57+
58+
def get(self, request):
59+
return self.error(err="problem-not-found", msg="Problem not exist")
60+
61+
response = BusinessErrorAPIView.as_view()(self.factory.get("/"))
62+
63+
self.assertEqual(response.status_code, 400)
64+
self.assertEqual(response.data, {"error": "problem-not-found", "data": "Problem not exist"})
65+
66+
3667
class CodePlaceCollectorTest(SimpleTestCase):
3768

3869
def _samples_by_metric(self, metrics):

frontend/src/pages/admin/api.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -467,10 +467,16 @@ function ajax(url, method, options) {
467467
}
468468
}
469469
},
470-
(res) => {
470+
(error) => {
471471
// API请求异常,一般为Server error 或 network error
472-
reject(res)
473-
Vue.prototype.$error(res.data.data)
472+
const res = error.response || error
473+
const data = res.data || {}
474+
const message = data.data || error.message || "Server error"
475+
Vue.prototype.$error(message)
476+
reject(error)
477+
if (typeof message === "string" && message.startsWith("Please login")) {
478+
router.push({ name: "login" })
479+
}
474480
},
475481
)
476482
})

frontend/src/pages/oj/api.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -514,10 +514,19 @@ function ajax(url, method, options) {
514514
// }
515515
}
516516
},
517-
(res) => {
517+
(error) => {
518518
// API请求异常,一般为Server error 或 network error
519-
reject(res)
520-
Vue.prototype.$error(res.data.data)
519+
const res = error.response || error
520+
const data = res.data || {}
521+
const message = data.data || error.message || "Server error"
522+
Vue.prototype.$error(message)
523+
reject(error)
524+
if (typeof message === "string" && message.startsWith("Please login")) {
525+
store.dispatch("changeModalStatus", {
526+
mode: "login",
527+
visible: true,
528+
})
529+
}
521530
},
522531
)
523532
})

0 commit comments

Comments
 (0)