Skip to content

Commit 87a2444

Browse files
committed
refactor: migrate visitor/main to typed models with live API contract tests
1 parent 0c36ebe commit 87a2444

7 files changed

Lines changed: 196 additions & 126 deletions

File tree

tests/4/test_id3_tagging_with_missing_id3.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
sys.path.insert(0, str(TESTS_DIR.parent))
99
sys.path.insert(0, str(TESTS_DIR.parent / "vistopia"))
1010
from vistopia.visitor import Visitor
11+
from vistopia.models import RetagArticle, RetagSeries, Catalog
1112

1213
CURR_TEST_DIR = Path(__file__).parent
1314

@@ -36,21 +37,25 @@ def test_id3_tagging_with_missing_id3(tmpdir):
3637

3738
Visitor.retag(
3839
test_mp3,
39-
article_info={
40-
"title": "测试标题",
41-
"sort_number": "7",
42-
"content_url": "http://example.com",
43-
},
44-
series_info={
45-
"title": "测试系列",
46-
"author": "测试作者",
47-
},
48-
catalog_info={}
40+
article_info=RetagArticle(
41+
title="测试标题",
42+
sort_number="7",
43+
content_url="http://example.com",
44+
),
45+
series_info=RetagSeries(
46+
title="测试系列",
47+
author="测试作者",
48+
),
49+
catalog_info=Catalog(
50+
author="测试作者",
51+
title="测试系列",
52+
type="charge",
53+
catalog=[],
54+
)
4955
)
5056

5157
tag = EasyID3(test_mp3)
5258

5359
assert tag["title"] == ["测试标题"]
5460
assert tag["album"] == ["测试系列"]
5561
assert tag["artist"] == ["测试作者"]
56-

tests/test_live_endpoints.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Live API contract checks for public (no-token) endpoints.
2+
3+
These tests intentionally hit the real Vistopia API to verify that the
4+
observed response shapes still match our pydantic models.
5+
"""
6+
7+
from urllib.parse import urljoin
8+
9+
import requests
10+
11+
from vistopia.models import Catalog, ContentShow, SearchResult, SubscriptionsList, validate_model
12+
13+
14+
BASE_URL = "https://api.vistopia.com.cn/api/v1/"
15+
TIMEOUT = 20
16+
17+
18+
def _get_data(uri: str, **params):
19+
query = {"api_token": ""}
20+
query.update(params)
21+
response = requests.get(urljoin(BASE_URL, uri), params=query, timeout=TIMEOUT)
22+
payload = response.json()
23+
assert payload["status"] == "success"
24+
assert "data" in payload
25+
return payload["data"]
26+
27+
28+
def test_live_catalog_endpoint_matches_model():
29+
data = _get_data("content/catalog/18")
30+
model = validate_model(Catalog, data)
31+
assert model.title
32+
assert model.catalog
33+
assert model.catalog[0].part
34+
35+
36+
def test_live_content_show_endpoint_matches_model():
37+
data = _get_data("content/content-show/18")
38+
model = validate_model(ContentShow, data)
39+
assert model.title
40+
assert model.author
41+
42+
43+
def test_live_search_endpoint_matches_model():
44+
data = _get_data("search/web", keyword="八分")
45+
model = validate_model(SearchResult, data)
46+
assert isinstance(model.data, list)
47+
assert len(model.data) > 0
48+
49+
50+
def test_live_subscriptions_endpoint_matches_model():
51+
data = _get_data("user/subscriptions-list")
52+
model = validate_model(SubscriptionsList, data)
53+
assert isinstance(model.data, list)

tests/test_models.py

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66
Catalog,
77
CatalogPart,
88
ContentShow,
9-
SearchPage,
109
SearchItem,
1110
SearchResult,
12-
SubscriptionsPage,
1311
SubscriptionItem,
1412
SubscriptionsList,
1513
validate_model,
@@ -30,9 +28,8 @@ def test_models_cover_all_keys_used_by_runtime_code():
3028
assert {"author", "title"} <= _field_names(ContentShow)
3129
assert {"data"} <= _field_names(SubscriptionsList)
3230
assert {"content_id", "title", "subtitle"} <= _field_names(SubscriptionItem)
33-
assert {"data"} <= _field_names(SearchResult)
34-
assert {"data", "current_page", "from_", "last_page"} <= _field_names(SearchPage)
35-
assert {"data", "current_page", "from_", "last_page"} <= _field_names(SubscriptionsPage)
31+
assert {"data", "current_page", "from_", "last_page"} <= _field_names(SearchResult)
32+
assert {"data", "current_page", "from_", "last_page"} <= _field_names(SubscriptionsList)
3633
assert {"id", "author", "title", "share_desc", "data_type", "subtitle"} <= _field_names(SearchItem)
3734

3835

@@ -67,31 +64,29 @@ def test_catalog_model_parses_nested_structure():
6764

6865
def test_search_result_coerces_numeric_id_from_string():
6966
payload = {
70-
"data": {
71-
"current_page": 1,
72-
"from": 1,
73-
"last_page": 1,
74-
"next_page_url": None,
75-
"per_page": 20,
76-
"prev_page_url": None,
77-
"to": 1,
78-
"total": 1,
79-
"data": [
80-
{
81-
"id": "11",
82-
"author": "梁文道",
83-
"title": "八分",
84-
"share_desc": "知识只求八分饱",
85-
"data_type": "content",
86-
"subtitle": "",
87-
}
88-
],
89-
}
67+
"current_page": 1,
68+
"from": 1,
69+
"last_page": 1,
70+
"next_page_url": None,
71+
"per_page": 20,
72+
"prev_page_url": None,
73+
"to": 1,
74+
"total": 1,
75+
"data": [
76+
{
77+
"id": "11",
78+
"author": "梁文道",
79+
"title": "八分",
80+
"share_desc": "知识只求八分饱",
81+
"data_type": "content",
82+
"subtitle": "",
83+
}
84+
],
9085
}
9186

9287
model = validate_model(SearchResult, payload)
93-
assert model.data.data[0].id == 11
94-
assert model.data.from_ == 1
88+
assert model.data[0].id == 11
89+
assert model.from_ == 1
9590

9691

9792
def test_catalog_model_raises_on_missing_required_fields():

tests/test_visitor.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ def visitor():
1616
def test_get_catalog(visitor):
1717
catalog = visitor.get_catalog(id=11)
1818
assert catalog
19-
assert catalog.get("author") == "梁文道"
20-
assert catalog.get("title") == "八分"
19+
assert catalog.author == "梁文道"
20+
assert catalog.title == "八分"
2121

22-
assert isinstance(catalog.get("catalog"), list)
22+
assert isinstance(catalog.catalog, list)
2323

2424

2525
def test_get_content_show(visitor):
2626
content_show = visitor.get_content_show(id=11)
27-
assert content_show.get("author") == "梁文道"
28-
assert content_show.get("title") == "八分"
27+
assert content_show.author == "梁文道"
28+
assert content_show.title == "八分"
2929

3030

3131
def test_save_show(visitor, tmpdir):
@@ -75,13 +75,13 @@ def test_search(visitor: Visitor, keyword: str, expected: tuple):
7575
data = visitor.search(keyword)
7676
assert isinstance(data, list)
7777
for k in ("id", "author", "title", "share_desc", "data_type"):
78-
assert all([k in item.keys() for item in data])
78+
assert all([hasattr(item, k) for item in data])
7979
assert any([
8080
(
81-
int(item["id"]),
82-
item["author"],
83-
item["title"],
84-
item["share_desc"],
85-
item["data_type"],
81+
int(item.id),
82+
item.author,
83+
item.title,
84+
item.share_desc,
85+
item.data_type,
8686
) == expected for item in data
8787
])

vistopia/main.py

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .visitor import Visitor
1111
from .utils import range_expand
1212
from .__version__ import __version__
13+
from .models import dump_model
1314

1415
logger = getLogger(__name__)
1516

@@ -51,19 +52,19 @@ def main(ctx: click.Context, **argv):
5152
def search(ctx: click.Context, **argv):
5253
visitor: Visitor = ctx.obj.visitor
5354
search_result_list = visitor.search(argv.pop("keyword"))
54-
logger.debug(json.dumps(search_result_list, indent=2, ensure_ascii=False))
55+
logger.debug(json.dumps([dump_model(item) for item in search_result_list], indent=2, ensure_ascii=False))
5556

5657
table = []
5758
for item in search_result_list:
58-
if item["data_type"] != "content":
59+
if item.data_type != "content":
5960
continue
60-
author = item["author"]
61-
if item["subtitle"]:
62-
title = "%s: %s" % ([item["title"], item["subtitle"]])
61+
author = item.author
62+
if item.subtitle:
63+
title = "%s: %s" % ([item.title, item.subtitle])
6364
else:
64-
title = item["title"]
65-
desc = item["share_desc"]
66-
content_id = item["id"]
65+
title = item.title
66+
desc = item.share_desc
67+
content_id = item.id
6768
table.append((content_id, author, title, desc))
6869

6970
click.echo(tabulate(table))
@@ -78,8 +79,8 @@ def subscriptions(ctx: click.Context):
7879

7980
table = []
8081
for show in visitor.get_user_subscriptions_list():
81-
title = ": ".join([show["title"], show["subtitle"]])
82-
content_id = show["content_id"]
82+
title = ": ".join([show.title, show.subtitle])
83+
content_id = show.content_id
8384
table.append((content_id, title))
8485

8586
click.echo(tabulate(table))
@@ -94,27 +95,27 @@ def show_content(ctx: click.Context, **argv):
9495
content_id = argv.pop("id")
9596
logger.debug(visitor.get_content_show(content_id))
9697
logger.debug(
97-
json.dumps(visitor.get_catalog(content_id), indent=2, ensure_ascii=False)
98+
json.dumps(dump_model(visitor.get_catalog(content_id)), indent=2, ensure_ascii=False)
9899
)
99100

100101
catalog = visitor.get_catalog(content_id)
101102

102-
click.echo(f"{catalog['title']}")
103+
click.echo(f"{catalog.title}")
103104
click.echo()
104-
click.echo(f"艺人: {catalog['author']}")
105-
click.echo(f"类型: {catalog['type']}")
105+
click.echo(f"艺人: {catalog.author}")
106+
click.echo(f"类型: {catalog.type}")
106107
click.echo()
107108

108-
for part in catalog["catalog"]:
109-
click.echo(f"{part['catalog_number']} {part['catalog_title']}")
109+
for part in catalog.catalog:
110+
click.echo(f"{part.catalog_number} {part.catalog_title}")
110111
table = []
111-
for article in part["part"]:
112+
for article in part.part:
112113
table.append(
113114
(
114-
article["sort_number"],
115+
article.sort_number,
115116
# article["article_id"],
116-
article["title"],
117-
article["duration_str"],
117+
article.title,
118+
article.duration_str,
118119
)
119120
)
120121
click.echo(tabulate(table))
@@ -132,7 +133,7 @@ def save_show(ctx: click.Context, **argv):
132133

133134
logger.debug(
134135
json.dumps(
135-
ctx.obj.visitor.get_catalog(content_id), indent=2, ensure_ascii=False
136+
dump_model(ctx.obj.visitor.get_catalog(content_id)), indent=2, ensure_ascii=False
136137
)
137138
)
138139

@@ -166,7 +167,7 @@ def save_transcript(ctx: click.Context, **argv):
166167

167168
logger.debug(
168169
json.dumps(
169-
ctx.obj.visitor.get_catalog(content_id), indent=2, ensure_ascii=False
170+
dump_model(ctx.obj.visitor.get_catalog(content_id)), indent=2, ensure_ascii=False
170171
)
171172
)
172173

0 commit comments

Comments
 (0)