Skip to content

Commit bb49392

Browse files
committed
feat(file): report a rejected file extension clearly on upload
- translate MediaWiki filetype-banned errors into a readable ValueError - name the offending extension and list the extensions the wiki accepts - re-raise every other APIError unchanged - closes #51
1 parent 0d32bc2 commit bb49392

2 files changed

Lines changed: 134 additions & 7 deletions

File tree

‎src/osw/controller/file/wiki.py‎

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,50 @@
22
import os
33
from typing import IO, Any, Dict, List, Optional
44

5+
import mwclient.errors
6+
57
from osw.controller.file.base import FileController
68
from osw.controller.file.remote import RemoteFileController
79
from osw.core import OSW, model
810
from osw.utils.wiki import get_namespace, get_title
911
from osw.wtsite import WtSite
1012

1113

14+
def get_allowed_file_extensions(mw_site) -> Optional[List[str]]:
15+
"""Queries the file extensions the wiki accepts, None if unavailable"""
16+
try:
17+
result = mw_site.api(
18+
"query", meta="siteinfo", siprop="fileextensions", formatversion=2
19+
)
20+
except Exception:
21+
# only used to enrich an error message, never worth failing over
22+
return None
23+
extensions = result.get("query", {}).get("fileextensions", [])
24+
return [entry["ext"] for entry in extensions if "ext" in entry]
25+
26+
27+
def reraise_upload_error(
28+
error: mwclient.errors.APIError, mw_site, title: str, suffix: Optional[str]
29+
) -> None:
30+
"""Turns a rejected file extension into a readable error, re-raises the rest
31+
32+
MediaWiki reports a rejected extension as filetype-banned,
33+
filetype-banned-type or filetype-badtype, depending on the version.
34+
"""
35+
if "filetype" not in str(getattr(error, "code", "")):
36+
raise error
37+
allowed = get_allowed_file_extensions(mw_site)
38+
hint = (
39+
f" Extensions allowed on this wiki: {', '.join(sorted(allowed))}."
40+
if allowed
41+
else ""
42+
)
43+
raise ValueError(
44+
f"Upload of '{title}' was rejected because the file extension "
45+
f"'{suffix}' is not allowed on {mw_site.host}.{hint}"
46+
) from error
47+
48+
1249
class WikiFileController(model.WikiFile, RemoteFileController):
1350
"""File controller for wiki files"""
1451

@@ -138,13 +175,16 @@ def put(self, file: IO, **kwargs: Dict[str, Any]):
138175
**se_params,
139176
)
140177
)
141-
self.osw.mw_site.upload(
142-
file=file,
143-
filename=self.title,
144-
# comment="",
145-
# description="",
146-
ignore=True,
147-
)
178+
try:
179+
self.osw.mw_site.upload(
180+
file=file,
181+
filename=self.title,
182+
# comment="",
183+
# description="",
184+
ignore=True,
185+
)
186+
except mwclient.errors.APIError as e:
187+
reraise_upload_error(e, self.osw.mw_site, self.title, self.suffix)
148188

149189
def put_from(self, other: FileController, **kwargs: Dict[str, Any]):
150190
# if isinstance(file, LocalFileController) and self.suffix is None:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Unit tests for upload error reporting in osw.controller.file.wiki.
2+
3+
Regression guard for #51: a file rejected because of its extension must produce
4+
an error that names the extension, instead of a bare MediaWiki API code.
5+
"""
6+
7+
import mwclient.errors
8+
import pytest
9+
10+
from osw.controller.file.wiki import (
11+
get_allowed_file_extensions,
12+
reraise_upload_error,
13+
)
14+
15+
16+
class _FakeSite:
17+
host = "wiki.example.org"
18+
19+
def __init__(self, extensions=None, fail=False):
20+
self._extensions = extensions
21+
self._fail = fail
22+
23+
def api(self, *args, **kwargs):
24+
if self._fail:
25+
raise RuntimeError("siteinfo unavailable")
26+
return {"query": {"fileextensions": [{"ext": e} for e in self._extensions]}}
27+
28+
29+
def _api_error(code):
30+
return mwclient.errors.APIError(code, "info", {})
31+
32+
33+
@pytest.mark.parametrize(
34+
"code", ["filetype-banned", "filetype-banned-type", "filetype-badtype"]
35+
)
36+
def test_rejected_extension_names_the_extension(code):
37+
site = _FakeSite(extensions=["png", "pdf"])
38+
39+
with pytest.raises(ValueError) as exc_info:
40+
reraise_upload_error(_api_error(code), site, "OSW123.exe", ".exe")
41+
42+
message = str(exc_info.value)
43+
assert ".exe" in message
44+
assert "OSW123.exe" in message
45+
assert "wiki.example.org" in message
46+
assert "pdf, png" in message # allowed extensions, sorted
47+
48+
49+
def test_original_error_is_chained():
50+
site = _FakeSite(extensions=["png"])
51+
original = _api_error("filetype-banned")
52+
53+
with pytest.raises(ValueError) as exc_info:
54+
reraise_upload_error(original, site, "OSW123.exe", ".exe")
55+
56+
assert exc_info.value.__cause__ is original
57+
58+
59+
def test_unrelated_api_error_is_reraised_unchanged():
60+
site = _FakeSite(extensions=["png"])
61+
original = _api_error("readapidenied")
62+
63+
with pytest.raises(mwclient.errors.APIError) as exc_info:
64+
reraise_upload_error(original, site, "OSW123.png", ".png")
65+
66+
assert exc_info.value is original
67+
68+
69+
def test_message_omits_the_hint_when_siteinfo_fails():
70+
"""A failing siteinfo lookup must not mask the upload error."""
71+
site = _FakeSite(fail=True)
72+
73+
with pytest.raises(ValueError) as exc_info:
74+
reraise_upload_error(_api_error("filetype-banned"), site, "a.exe", ".exe")
75+
76+
assert "allowed on this wiki" not in str(exc_info.value)
77+
assert ".exe" in str(exc_info.value)
78+
79+
80+
def test_get_allowed_file_extensions_returns_none_on_failure():
81+
assert get_allowed_file_extensions(_FakeSite(fail=True)) is None
82+
83+
84+
def test_get_allowed_file_extensions_reads_siteinfo():
85+
site = _FakeSite(extensions=["png", "jpg"])
86+
87+
assert get_allowed_file_extensions(site) == ["png", "jpg"]

0 commit comments

Comments
 (0)