Skip to content

Commit f230fa8

Browse files
author
Antoine
committed
Refactor HTTP status codes and add comprehensive tests for all categories
1 parent 3316833 commit f230fa8

3 files changed

Lines changed: 226 additions & 4 deletions

File tree

http_code/http_code.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
class HttpCode(IntEnum):
44
"""HTTP status codes, compatible with Flask"""
55
# Informational
6-
CONTINUE = 101 # continue processing
7-
SWITCH_PROTOCOL = 102 # switch protocol
8-
PROCESSING = 103 # tell the client that the server is processing the request, but no response is available yet
6+
CONTINUE = 100 # continue processing
7+
SWITCH_PROTOCOL = 101 # switch protocol
8+
PROCESSING = 102 # tell the client that the server is processing the request, but no response is available yet
99
EARLY_HINTS = 103 # allow to start the preloading of resources before the server has finished processing the request
1010

1111
# Success
@@ -20,6 +20,9 @@ class HttpCode(IntEnum):
2020
NO_CONTENT = 204 # request was successful, but no content is returned (headers may still be useful)
2121
RESET_CONTENT = 205 # request was successful, but the client should reset the document view
2222
PARTIAL_CONTENT = 206 # request was successful, but only a part of the resource is returned (used for range requests)
23+
MULTI_STATUS = 207 # used in WebDAV to provide information about multiple resources in a single response
24+
ALREADY_REPORTED = 208 # used in WebDAV to indicate that the members of a collection have already been reported in a previous response
25+
IM_USED = 226 # indicates that the server is returning a delta in response to a GET request. It is used in the context of HTTP delta encodings
2326

2427
# Redirection
2528
MULTIPLE_CHOICES = 300 # client must take additional action to complete the request
@@ -33,6 +36,7 @@ class HttpCode(IntEnum):
3336
# Client Error
3437
BAD_REQUEST = 400 # server could not understand the request due to invalid syntax
3538
UNAUTHORIZED = 401 # client must authenticate itself to get the requested response
39+
PAYMENT_REQUIRED = 402 # nonstandard response status code reserved for future use.
3640
FORBIDDEN = 403 # client does not have permission to access the requested resource
3741
NOT_FOUND = 404 # server cannot find the requested resource. (May be used to hide the existence of a resource; in this case, it replace 403 Forbidden)
3842
METHOD_NOT_ALLOWED = 405 # request method is not supported for the requested resource
@@ -44,11 +48,45 @@ class HttpCode(IntEnum):
4448
LENGTH_REQUIRED = 411 # server requires a content-length header
4549
PRECONDITION_FAILED = 412 # one or more conditions in the request header fields evaluated to false
4650
PAYLOAD_TOO_LARGE = 413 # request is larger than the server is willing or able to process
51+
URI_TOO_LONG = 414 # request URI is longer than the server is willing to interpret
52+
UNSUPPORTED_MEDIA_TYPE = 415 # request entity has a media type which the server or resource does not support
53+
RANGE_NOT_SATISFIABLE = 416 # server cannot provide the requested range (used in range requests)
54+
EXPECTATION_FAILED = 417 # expectation given in the request's Expect header field cannot be met by the server
4755
IM_A_TEAPOT = 418 # server refuses to brew coffee because it is a teapot
56+
MISDIRECTED_REQUEST = 421 # request was directed at a server that is not able to produce a response (e.g., due to connection reuse)
57+
UNPROCESSABLE_CONTENT = 422 # request was well-formed, but was unable to be followed due to semantic errors
58+
LOCKED = 423 # resource that is being accessed is locked
59+
FAILED_DEPENDENCY = 424 # request failed due to failure of a previous request (used in WebDAV)
60+
# TOO_EARLY = 425 # server is unwilling to risk processing a request that might be replayed (firefox only)
61+
UPGRADE_REQUIRED = 426 # client should switch to a different protocol (e.g., TLS/1.0)
62+
PRECONDITION_REQUIRED = 428 # origin server requires the request to be conditional (used in WebDAV)
4863
TOO_MANY_REQUESTS = 429 # client has sent too many requests in a given amount of time
64+
REQUEST_HEADER_FIELDS_TOO_LARGE = 431 # server is unwilling to process the request because its header fields are too large
65+
UNAVAILABLE_FOR_LEGAL_REASONS = 451 # resource is unavailable for legal reasons (e.g., censorship)
4966

5067
# Server Error
5168
INTERNAL_SERVER_ERROR = 500 # server has encountered a situation it doesn't know how to handle (only if no other error code applies)
5269
NOT_IMPLEMENTED = 501 # request method is not supported by the server and cannot be handled
70+
BAD_GATEWAY = 502 # server, while acting as a gateway or proxy, received an invalid response from the upstream server
5371
SERVICE_UNAVAILABLE = 503 # server is not ready to handle the request (commonly used for maintenance)
54-
HTTP_VERSION_NOT_SUPPORTED = 505 # server does not support the HTTP protocol version used in the request
72+
GATEWAY_TIMEOUT = 504 # server is acting as a gateway and cannot get a response in time from the upstream server
73+
HTTP_VERSION_NOT_SUPPORTED = 505 # server does not support the HTTP protocol version used in the request
74+
VARIANT_ALSO_NEGOTIATES = 506 # server has an internal configuration error: transparent content negotiation for the request results in a circular reference
75+
INSUFFICIENT_STORAGE = 507 # server is unable to store the representation needed to complete the request
76+
LOOP_DETECTED = 508 # server detected an infinite loop while processing a request (used in WebDAV)
77+
NOT_EXTENDED = 510 # server requires further extensions to fulfill the request
78+
NETWORK_AUTHENTICATION_REQUIRED = 511 # client needs to authenticate to gain network access (used in captive portals)
79+
80+
def label(self) -> str:
81+
"""
82+
Get the label of the HTTP status code.
83+
:return: Label of the HTTP status code.
84+
"""
85+
return self.name.replace('_', ' ').title()
86+
87+
def __str__(self) -> str:
88+
"""
89+
Get the string representation of the HTTP status code.
90+
:return: String representation of the HTTP status code.
91+
"""
92+
return f"{self.value} {self.label()}"

http_code/tests.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
2+
from http_code import HttpCode
3+
import pytest
4+
5+
6+
@pytest.mark.parametrize(
7+
"code, expected_name, expected_label",
8+
[
9+
(HttpCode.CONTINUE, "CONTINUE", "Continue"),
10+
(HttpCode.SWITCH_PROTOCOL, "SWITCH_PROTOCOL", "Switch Protocol"),
11+
(HttpCode.PROCESSING, "PROCESSING", "Processing"),
12+
(HttpCode.EARLY_HINTS, "EARLY_HINTS", "Early Hints"),
13+
],
14+
ids=[
15+
"100 Continue",
16+
"101 Switch Protocol",
17+
"102 Processing",
18+
"103 Early Hints",
19+
]
20+
)
21+
def test_informational_codes(code, expected_name, expected_label):
22+
"""Test informational HTTP status codes"""
23+
assert code == getattr(HttpCode, expected_name)
24+
assert code.label() == expected_label
25+
assert str(code) == f"{code.value} {expected_label}"
26+
27+
28+
@pytest.mark.parametrize(
29+
"code, expected_name, expected_label",
30+
[
31+
(HttpCode.OK, "OK", "Ok"),
32+
(HttpCode.CREATED, "CREATED", "Created"),
33+
(HttpCode.ACCEPTED, "ACCEPTED", "Accepted"),
34+
(HttpCode.NON_AUTHORITATIVE_INFORMATION, "NON_AUTHORITATIVE_INFORMATION", "Non Authoritative Information"),
35+
(HttpCode.NO_CONTENT, "NO_CONTENT", "No Content"),
36+
(HttpCode.RESET_CONTENT, "RESET_CONTENT", "Reset Content"),
37+
(HttpCode.PARTIAL_CONTENT, "PARTIAL_CONTENT", "Partial Content"),
38+
(HttpCode.MULTI_STATUS, "MULTI_STATUS", "Multi Status"),
39+
(HttpCode.ALREADY_REPORTED, "ALREADY_REPORTED", "Already Reported"),
40+
(HttpCode.IM_USED, "IM_USED", "Im Used"),
41+
],
42+
ids=[
43+
"200 OK",
44+
"201 Created",
45+
"202 Accepted",
46+
"203 Non-Authoritative Information",
47+
"204 No Content",
48+
"205 Reset Content",
49+
"206 Partial Content",
50+
"207 Multi-Status",
51+
"208 Already Reported",
52+
"226 IM Used",
53+
]
54+
)
55+
def test_success_codes(code, expected_name, expected_label):
56+
"""Test informational HTTP status codes"""
57+
assert code == getattr(HttpCode, expected_name)
58+
assert code.label() == expected_label
59+
assert str(code) == f"{code.value} {expected_label}"
60+
61+
62+
@pytest.mark.parametrize(
63+
"code, expected_name, expected_label",
64+
[
65+
(HttpCode.MULTIPLE_CHOICES, "MULTIPLE_CHOICES", "Multiple Choices"),
66+
(HttpCode.MOVED_PERMANENTLY, "MOVED_PERMANENTLY", "Moved Permanently"),
67+
(HttpCode.FOUND, "FOUND", "Found"),
68+
(HttpCode.SEE_OTHER, "SEE_OTHER", "See Other"),
69+
(HttpCode.NOT_MODIFIED, "NOT_MODIFIED", "Not Modified"),
70+
(HttpCode.TEMPORARY_REDIRECT, "TEMPORARY_REDIRECT", "Temporary Redirect"),
71+
(HttpCode.PERMANENT_REDIRECT, "PERMANENT_REDIRECT", "Permanent Redirect"),
72+
],
73+
ids=[
74+
"300 Multiple Choices",
75+
"301 Moved Permanently",
76+
"302 Found",
77+
"303 See Other",
78+
"304 Not Modified",
79+
"307 Temporary Redirect",
80+
"308 Permanent Redirect",
81+
]
82+
)
83+
def test_redirection_codes(code, expected_name, expected_label):
84+
"""Test redirection HTTP status codes"""
85+
assert code == getattr(HttpCode, expected_name)
86+
assert code.label() == expected_label
87+
assert str(code) == f"{code.value} {expected_label}"
88+
89+
90+
@pytest.mark.parametrize(
91+
"code, expected_name, expected_label",
92+
[
93+
(HttpCode.BAD_REQUEST, "BAD_REQUEST", "Bad Request"),
94+
(HttpCode.UNAUTHORIZED, "UNAUTHORIZED", "Unauthorized"),
95+
(HttpCode.PAYMENT_REQUIRED, "PAYMENT_REQUIRED", "Payment Required"),
96+
(HttpCode.FORBIDDEN, "FORBIDDEN", "Forbidden"),
97+
(HttpCode.NOT_FOUND, "NOT_FOUND", "Not Found"),
98+
(HttpCode.METHOD_NOT_ALLOWED, "METHOD_NOT_ALLOWED", "Method Not Allowed"),
99+
(HttpCode.NOT_ACCEPTABLE, "NOT_ACCEPTABLE", "Not Acceptable"),
100+
(HttpCode.PROXY_AUTHENTICATION_REQUIRED, "PROXY_AUTHENTICATION_REQUIRED", "Proxy Authentication Required"),
101+
(HttpCode.REQUEST_TIMEOUT, "REQUEST_TIMEOUT", "Request Timeout"),
102+
(HttpCode.CONFLICT, "CONFLICT", "Conflict"),
103+
(HttpCode.GONE, "GONE", "Gone"),
104+
(HttpCode.LENGTH_REQUIRED, "LENGTH_REQUIRED", "Length Required"),
105+
(HttpCode.PRECONDITION_FAILED, "PRECONDITION_FAILED", "Precondition Failed"),
106+
(HttpCode.PAYLOAD_TOO_LARGE, "PAYLOAD_TOO_LARGE", "Payload Too Large"),
107+
(HttpCode.URI_TOO_LONG, "URI_TOO_LONG", "Uri Too Long"),
108+
(HttpCode.UNSUPPORTED_MEDIA_TYPE, "UNSUPPORTED_MEDIA_TYPE", "Unsupported Media Type"),
109+
(HttpCode.RANGE_NOT_SATISFIABLE, "RANGE_NOT_SATISFIABLE", "Range Not Satisfiable"),
110+
(HttpCode.EXPECTATION_FAILED, "EXPECTATION_FAILED", "Expectation Failed"),
111+
(HttpCode.IM_A_TEAPOT, 'IM_A_TEAPOT', 'Im A Teapot'), # RFC 7168
112+
(HttpCode.MISDIRECTED_REQUEST, 'MISDIRECTED_REQUEST', 'Misdirected Request'),
113+
(HttpCode.UNPROCESSABLE_CONTENT, 'UNPROCESSABLE_CONTENT', 'Unprocessable Content'),
114+
(HttpCode.LOCKED, 'LOCKED', 'Locked'),
115+
(HttpCode.FAILED_DEPENDENCY, 'FAILED_DEPENDENCY', 'Failed Dependency'),
116+
],
117+
ids=[
118+
"400 Bad Request",
119+
"401 Unauthorized",
120+
"402 Payment Required",
121+
"403 Forbidden",
122+
"404 Not Found",
123+
"405 Method",
124+
"406 Not Acceptable",
125+
"407 Proxy Authentication Required",
126+
"408 Request Timeout",
127+
"409 Conflict",
128+
"410 Gone",
129+
"411 Length Required",
130+
"412 Precondition Failed",
131+
"413 Payload Too Large",
132+
"414 URI Too Long",
133+
"415 Unsupported Media Type",
134+
"416 Range Not Satisfiable",
135+
"417 Expectation failed",
136+
"418 Im A Teapot",
137+
"421 Misdirected Request",
138+
"422 Unprocessable Content",
139+
"423 Locked",
140+
"424 Failed Dependency"
141+
]
142+
)
143+
def test_client_error_codes(code, expected_name, expected_label):
144+
"""Test client error HTTP status codes"""
145+
assert code == getattr(HttpCode, expected_name)
146+
assert code.label() == expected_label
147+
assert str(code) == f"{code.value} {expected_label}"
148+
149+
150+
@pytest.mark.parametrize(
151+
"code, expected_name, expected_label",
152+
[
153+
(HttpCode.INTERNAL_SERVER_ERROR, "INTERNAL_SERVER_ERROR", "Internal Server Error"),
154+
(HttpCode.NOT_IMPLEMENTED, "NOT_IMPLEMENTED", "Not Implemented"),
155+
(HttpCode.BAD_GATEWAY, "BAD_GATEWAY", "Bad Gateway"),
156+
(HttpCode.SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", "Service Unavailable"),
157+
(HttpCode.GATEWAY_TIMEOUT, "GATEWAY_TIMEOUT", "Gateway Timeout"),
158+
(HttpCode.HTTP_VERSION_NOT_SUPPORTED, "HTTP_VERSION_NOT_SUPPORTED", "Http Version Not Supported"),
159+
(HttpCode.VARIANT_ALSO_NEGOTIATES, "VARIANT_ALSO_NEGOTIATES", "Variant Also Negotiates"),
160+
(HttpCode.INSUFFICIENT_STORAGE, "INSUFFICIENT_STORAGE", "Insufficient Storage"),
161+
(HttpCode.LOOP_DETECTED, "LOOP_DETECTED", "Loop Detected"),
162+
(HttpCode.NOT_EXTENDED, "NOT_EXTENDED", "Not Extended"),
163+
(HttpCode.NETWORK_AUTHENTICATION_REQUIRED, 'NETWORK_AUTHENTICATION_REQUIRED', 'Network Authentication Required'),
164+
],
165+
ids=[
166+
"500 Internal Server Error",
167+
"501 Not Implemented",
168+
"502 Bad Gateway",
169+
"503 Service Unavailable",
170+
"504 Gateway Timeout",
171+
"505 Http Version Not Supported",
172+
"506 Variant Also Negotiates",
173+
"507 Insufficient Storage",
174+
"508 Loop Detected",
175+
"510 Not Extended",
176+
'511 Network Authentication Required'
177+
]
178+
)
179+
def test_server_error_codes(code, expected_name, expected_label):
180+
"""Test server error HTTP status codes"""
181+
assert code == getattr(HttpCode, expected_name)
182+
assert code.label() == expected_label
183+
assert str(code) == f"{code.value} {expected_label}"

makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ tests/%: % #with pytest
1818
-coverage run -m --branch pytest --tb=short --disable-warnings --junitxml=tests_reports/$*/report.xml $<
1919
@coverage report -m
2020
@coverage html -d tests_reports/$*/coverage
21+
@rm .coverage
2122

2223
tests: $(addprefix tests/,$(MODULES))
2324

0 commit comments

Comments
 (0)