Skip to content

Commit b8c71f5

Browse files
authored
Add mock github api (#352)
* wip add mock github api * add new files * wip use mock github api in tests * wip update tests * finish updating tests * more test cleanup * fix lint * clean up handling of mock github url
1 parent 81bbbf7 commit b8c71f5

File tree

9 files changed

+392
-258
lines changed

9 files changed

+392
-258
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ repos:
5757
- id: mypy
5858
args: ["--config-file", "pyproject.toml"]
5959
additional_dependencies:
60-
[pytest, click, importlib_resources, types-requests]
60+
[pytest, click, importlib_resources, types-requests, fastapi]
6161
stages: [manual]
6262

6363
- repo: https://github.com/sirosen/check-jsonschema

jupyter_releaser/mock_github.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import atexit
2+
import datetime
3+
import os
4+
import tempfile
5+
import uuid
6+
from typing import Dict, List
7+
8+
from fastapi import FastAPI, Request
9+
from fastapi.staticfiles import StaticFiles
10+
from pydantic import BaseModel
11+
12+
from jupyter_releaser.util import MOCK_GITHUB_URL
13+
14+
app = FastAPI()
15+
16+
static_dir = tempfile.TemporaryDirectory()
17+
atexit.register(static_dir.cleanup)
18+
app.mount("/static", StaticFiles(directory=static_dir.name), name="static")
19+
20+
releases: Dict[int, "Release"] = {}
21+
pulls: Dict[int, "PullRequest"] = {}
22+
release_ids_for_asset: Dict[int, int] = {}
23+
tag_refs: Dict[str, "Tag"] = {}
24+
25+
26+
class Asset(BaseModel):
27+
id: int
28+
name: str
29+
content_type: str
30+
size: int
31+
state: str = "uploaded"
32+
url: str
33+
node_id: str = ""
34+
download_count: int = 0
35+
label: str = ""
36+
uploader: None = None
37+
browser_download_url: str = ""
38+
created_at: str = ""
39+
updated_at: str = ""
40+
41+
42+
class Release(BaseModel):
43+
assets_url: str = ""
44+
upload_url: str
45+
tarball_url: str = ""
46+
zipball_url: str = ""
47+
created_at: str
48+
published_at: str = ""
49+
draft: bool
50+
body: str = ""
51+
id: int
52+
node_id: str = ""
53+
author: str = ""
54+
html_url: str
55+
name: str = ""
56+
prerelease: bool
57+
tag_name: str
58+
target_commitish: str
59+
assets: List[Asset]
60+
url: str
61+
62+
63+
class User(BaseModel):
64+
login: str = "bar"
65+
html_url: str = "http://bar.com"
66+
67+
68+
class PullRequest(BaseModel):
69+
number: int = 0
70+
html_url: str = "http://foo.com"
71+
title: str = "foo"
72+
user: User = User()
73+
74+
75+
class TagObject(BaseModel):
76+
sha: str
77+
78+
79+
class Tag(BaseModel):
80+
ref: str
81+
object: TagObject
82+
83+
84+
@app.get("/")
85+
def read_root():
86+
return {"Hello": "World"}
87+
88+
89+
@app.get("/repos/{owner}/{repo}/releases")
90+
def list_releases(owner: str, repo: str) -> List[Release]:
91+
"""https://docs.github.com/en/rest/releases/releases#list-releases"""
92+
return list(releases.values())
93+
94+
95+
@app.post("/repos/{owner}/{repo}/releases")
96+
async def create_a_release(owner: str, repo: str, request: Request) -> Release:
97+
"""https://docs.github.com/en/rest/releases/releases#create-a-release"""
98+
release_id = uuid.uuid4().int
99+
data = await request.json()
100+
url = f"https://github.com/repos/{owner}/{repo}/releases/{release_id}"
101+
html_url = f"https://github.com/{owner}/{repo}/releases/tag/{data['tag_name']}"
102+
upload_url = f"{MOCK_GITHUB_URL}/repos/{owner}/{repo}/releases/{release_id}/assets"
103+
fmt_str = r"%Y-%m-%dT%H:%M:%SZ"
104+
created_at = datetime.datetime.utcnow().strftime(fmt_str)
105+
model = Release(
106+
id=release_id,
107+
url=url,
108+
html_url=html_url,
109+
assets=[],
110+
upload_url=upload_url,
111+
created_at=created_at,
112+
**data,
113+
)
114+
releases[model.id] = model
115+
return model
116+
117+
118+
@app.patch("/repos/{owner}/{repo}/releases/{release_id}")
119+
async def update_a_release(owner: str, repo: str, release_id: int, request: Request) -> Release:
120+
"""https://docs.github.com/en/rest/releases/releases#update-a-release"""
121+
data = await request.json()
122+
model = releases[release_id]
123+
for name, value in data.items():
124+
setattr(model, name, value)
125+
return model
126+
127+
128+
@app.post("/repos/{owner}/{repo}/releases/{release_id}/assets")
129+
async def upload_a_release_asset(owner: str, repo: str, release_id: int, request: Request) -> None:
130+
"""https://docs.github.com/en/rest/releases/assets#upload-a-release-asset"""
131+
model = releases[release_id]
132+
asset_id = uuid.uuid4().int
133+
name = request.query_params["name"]
134+
with open(f"{static_dir.name}/{asset_id}", "wb") as fid:
135+
async for chunk in request.stream():
136+
fid.write(chunk)
137+
headers = request.headers
138+
url = f"{MOCK_GITHUB_URL}/static/{asset_id}"
139+
asset = Asset(
140+
id=asset_id,
141+
name=name,
142+
size=headers["content-length"],
143+
url=url,
144+
content_type=headers["content-type"],
145+
)
146+
release_ids_for_asset[asset_id] = release_id
147+
model.assets.append(asset)
148+
149+
150+
@app.delete("/repos/{owner}/{repo}/releases/assets/{asset_id}")
151+
async def delete_a_release_asset(owner: str, repo: str, asset_id: int) -> None:
152+
"""https://docs.github.com/en/rest/releases/assets#delete-a-release-asset"""
153+
release = releases[release_ids_for_asset[asset_id]]
154+
os.remove(f"{static_dir.name}/{asset_id}")
155+
release.assets = [a for a in release.assets if a.id != asset_id]
156+
157+
158+
@app.delete("/repos/{owner}/{repo}/releases/{release_id}")
159+
def delete_a_release(owner: str, repo: str, release_id: int) -> None:
160+
"""https://docs.github.com/en/rest/releases/releases#delete-a-release"""
161+
del releases[release_id]
162+
163+
164+
@app.get("/repos/{owner}/{repo}/pulls/{pull_number}")
165+
def get_a_pull_request(owner: str, repo: str, pull_number: int) -> PullRequest:
166+
"""https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request"""
167+
if pull_number not in pulls:
168+
pulls[pull_number] = PullRequest()
169+
return pulls[pull_number]
170+
171+
172+
@app.post("/repos/{owner}/{repo}/pulls")
173+
def create_a_pull_request(owner: str, repo: str) -> PullRequest:
174+
"""https://docs.github.com/en/rest/pulls/pulls#create-a-pull-request"""
175+
pull = PullRequest()
176+
pulls[pull.number] = pull
177+
return pull
178+
179+
180+
@app.post("/repos/{owner}/{repo}/issues/{issue_number}/labels")
181+
def add_labels_to_an_issue(owner: str, repo: str, issue_number: int) -> BaseModel:
182+
"""https://docs.github.com/en/rest/issues/labels#add-labels-to-an-issue"""
183+
return BaseModel()
184+
185+
186+
@app.post("/create_tag_ref/{tag_ref}/{sha}")
187+
def create_tag_ref(tag_ref: str, sha: str) -> None:
188+
"""Create a remote tag ref object for testing"""
189+
tag = Tag(ref=f"refs/tags/{tag_ref}", object=TagObject(sha=sha))
190+
tag_refs[tag_ref] = tag
191+
192+
193+
@app.get("/repos/{owner}/{repo}/git/matching-refs/tags/{tag_ref}")
194+
def list_matching_references(owner: str, repo: str, tag_ref: str) -> List[Tag]:
195+
"""https://docs.github.com/en/rest/git/refs#list-matching-references"""
196+
# raise ValueError("we should have an api to set a sha for a tag ref for tests")
197+
return [tag_refs[tag_ref]]

jupyter_releaser/tests/conftest.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
import os
55
import os.path as osp
66
from pathlib import Path
7-
from urllib.request import OpenerDirector
87

98
from click.testing import CliRunner
9+
from ghapi import core
1010
from pytest import fixture
1111

1212
from jupyter_releaser import cli, util
1313
from jupyter_releaser.tests import util as testutil
14-
from jupyter_releaser.util import run
14+
from jupyter_releaser.util import MOCK_GITHUB_URL, run, start_mock_github
1515

1616

1717
@fixture(autouse=True)
@@ -26,6 +26,7 @@ def mock_env(mocker):
2626
del env[key]
2727

2828
mocker.patch.dict(os.environ, env, clear=True)
29+
core.GH_HOST = MOCK_GITHUB_URL
2930

3031
try:
3132
run("git config --global user.name")
@@ -169,13 +170,6 @@ def git_prep(runner, git_repo):
169170
runner(["prep-git", "--git-url", git_repo])
170171

171172

172-
@fixture
173-
def open_mock(mocker):
174-
open_mock = mocker.patch.object(OpenerDirector, "open", autospec=True)
175-
open_mock.return_value = testutil.MockHTTPResponse()
176-
yield open_mock
177-
178-
179173
@fixture
180174
def build_mock(mocker):
181175
orig_run = util.run
@@ -193,3 +187,12 @@ def wrapped(cmd, **kwargs):
193187
return orig_run(cmd, **kwargs)
194188

195189
mock_run = mocker.patch("jupyter_releaser.util.run", wraps=wrapped)
190+
191+
192+
@fixture
193+
def mock_github():
194+
proc = start_mock_github()
195+
yield proc
196+
197+
proc.kill()
198+
proc.wait()

0 commit comments

Comments
 (0)