Skip to content

Commit 49dd9a2

Browse files
add integration tests for management command and adjust command
also some docs
1 parent c5643df commit 49dd9a2

File tree

11 files changed

+391
-82
lines changed

11 files changed

+391
-82
lines changed

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
TEST_ACCOUNT_NAME=
2+
TEST_ACCOUNT_TYPE=
3+
TEST_APP_ID=
4+
TEST_CLIENT_ID=
5+
TEST_INSTALLATION_ID=
6+
TEST_NAME=
7+
TEST_PRIVATE_KEY=""
8+
TEST_WEBHOOK_SECRET=""

CONTRIBUTING.md

Lines changed: 135 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,166 @@
22

33
All contributions are welcome! Besides code contributions, this includes things like documentation improvements, bug reports, and feature requests.
44

5-
You should first check if there is a [GitHub issue](https://github.com/joshuadavidthomas/django-bird/issues) already open or related to what you would like to contribute. If there is, please comment on that issue to let others know you are working on it. If there is not, please open a new issue to discuss your contribution.
5+
You should first check if there is a [GitHub issue](https://github.com/joshuadavidthomas/django-github-app/issues) already open or related to what you would like to contribute. If there is, please comment on that issue to let others know you are working on it. If there is not, please open a new issue to discuss your contribution.
66

77
Not all contributions need to start with an issue, such as typo fixes in documentation or version bumps to Python or Django that require no internal code changes, but generally, it is a good idea to open an issue first.
88

99
We adhere to Django's Code of Conduct in all interactions and expect all contributors to do the same. Please read the [Code of Conduct](https://www.djangoproject.com/conduct/) before contributing.
1010

11-
## Setup
11+
### Requirements
1212

13-
The following setup steps assume you are using a Unix-like operating system, such as Linux or macOS, and that you have a [supported](https://django-bird.readthedocs.io/#requirements) version of Python installed. If you are using Windows, you will need to adjust the commands accordingly. If you do not have Python installed, you can visit [python.org](https://www.python.org/) for instructions on how to install it for your operating system.
13+
- [uv](https://github.com/astral-sh/uv) - Modern Python toolchain that handles:
14+
- Python version management and installation
15+
- Virtual environment creation and management
16+
- Fast, reliable dependency resolution and installation
17+
- Reproducible builds via lockfile
18+
- [direnv](https://github.com/direnv/direnv) (Optional) - Automatic environment variable loading
19+
- [just](https://github.com/casey/just) (Optional) - Command runner for development tasks
1420

15-
1. Fork the repository and clone it locally.
16-
2. Create a virtual environment and activate it. You can use whatever tool you prefer for this. Below is an example using the Python standard library's `venv` module:
21+
### `Justfile`
22+
23+
The repository includes a `Justfile` that provides all common development tasks with a consistent interface. Running `just` without arguments shows all available commands and their descriptions:
24+
25+
```bash
26+
just
27+
# or explicitly
28+
just --list --list-submodules
29+
```
1730

18-
```shell
19-
python -m venv venv
20-
source venv/bin/activate
31+
<!-- [[[cog
32+
import subprocess
33+
import cog
34+
35+
output_raw = subprocess.run(['just', '--list', '--list-submodules'], stdout=subprocess.PIPE)
36+
output_list = output_raw.stdout.decode('utf-8').split('\n')
37+
38+
cog.out('```bash\n')
39+
for i, line in enumerate(output_list):
40+
if not line:
41+
continue
42+
cog.out(line)
43+
if i < len(output_list):
44+
cog.out('\n')
45+
cog.out('```')
46+
]]] -->
47+
```bash
48+
Available recipes:
49+
bootstrap
50+
coverage *ARGS
51+
lint
52+
lock *ARGS
53+
manage *COMMAND
54+
test *ARGS
55+
testall *ARGS
56+
types *ARGS
57+
docs:
58+
build LOCATION="docs/_build/html" # Build documentation using Sphinx
59+
serve PORT="8000" # Serve documentation locally
2160
```
61+
<!-- [[[end]]] -->
62+
63+
All commands below will contain the full command as well as its `just` counterpart.
64+
65+
## Setup
66+
67+
The following instructions will use `uv` and assume a Unix-like operating system (Linux or macOS).
2268

23-
3. Install `django-bird` and the `dev` dependencies in editable mode:
69+
Windows users will need to adjust commands accordingly, though the core workflow remains the same.
70+
Alternatively, any Python package manager that supports installing from `pyproject.toml` ([PEP 621](https://peps.python.org/pep-0621/)) can be used. If not using `uv`, ensure you have Python installed from [python.org](https://www.python.org/).
71+
72+
1. Fork the repository and clone it locally.
73+
2. Use `uv` too bootstrap your development environment:
2474

25-
```shell
26-
python -m pip install --editable '.[dev]'
27-
# or using [just](#just)
75+
```bash
76+
uv python install
77+
uv sync --locked
78+
# or
2879
just bootstrap
2980
```
3081

31-
## Testing
82+
This will install the correct Python version, create and configure a virtual environment, and install all dependencies.
3283

33-
We use [`pytest`](https://docs.pytest.org/) for testing and [`nox`](https://nox.thea.codes/) to run the tests in multiple environments.
84+
## Tests
85+
86+
The project uses [`pytest`](https://docs.pytest.org/) for testing and [`nox`](https://nox.thea.codes/) to run the tests in multiple environments.
3487

3588
To run the test suite against the default versions of Python (lower bound of supported versions) and Django (lower bound of LTS versions), run:
3689

37-
```shell
38-
python -m nox --session "test"
39-
# or using [just](#just)
90+
```bash
91+
uv run nox --session test
92+
# or
4093
just test
4194
```
4295

43-
To run the test suite against all supported versions of Python and Django, run:
96+
To run the test suite against the entire matrix of supported versions of Python and Django, run:
4497

45-
```shell
46-
python -m nox --session "tests"
47-
# or using [just](#just)
98+
```bash
99+
uv run nox --session tests
100+
# or
48101
just testall
49102
```
50103

51-
## `just`
52-
53-
[`just`](https://github.com/casey/just) is a command runner that is used to run common commands, similar to `make` or `invoke`. A `Justfile` is provided at the base of the repository, which contains commands for common development tasks, such as running the test suite or linting.
104+
Both can be passed additional arguments that will be provided to pytest:
54105

55-
To see a list of all available commands, ensure `just` is installed and run the following command at the base of the repository:
106+
```bash
107+
uv run nox --session test -- -v --last-failed
108+
uv run nox --session tests -- --failed-first --maxfail=1
109+
# or
110+
just test -v --last-failed
111+
just testall --failed-first --maxfail=1
112+
```
56113

57-
```shell
58-
just
114+
### Integration Tests
115+
116+
Integration tests are contained in the [`tests/integration`](tests/integration) directory and test actual interactions with GitHub's API and webhooks.
117+
118+
These tests are skipped by default and must be explicitly enabled by passing `--integration` as a pytest argument. Running them requires both a GitHub App and a Personal Access Token (PAT) configured in your environment.
119+
120+
Follow these steps to set up your environment for integration tests:
121+
122+
1. Create a test GitHub App:
123+
- Go to GitHub Developer Settings > GitHub Apps > New GitHub App
124+
- Set the Name to `@<username> - django-github-app tests` (this can be anything, but needs to be unique across GitHub and under 34 characters in length)
125+
- Set Homepage URL to your fork's URL, e.g. `https://github.com/<username>/django-github-app`
126+
- Turn webhooks off by toggling the Active checkbox under Webhook (there are currently no Webhook integration tests, this may need to be adjusted if/when they are added)
127+
- Set the following permissions:
128+
- Repository permissions:
129+
- Metadata: Read only
130+
- Select "Only on this account" for where the GitHub App can be installed
131+
2. After creating the test GitHub App:
132+
- Grab the following information from the admin panel:
133+
- App ID
134+
- Client ID
135+
- Generate and download private key
136+
3. Install the new test GitHub App on your user account by selecting "Install App" from the GitHub App admin panel.
137+
- After installation, make note of the unique ID in the URL, e.g. `https://github.com/settings/installations/<unique ID>`
138+
5. Configure the following environment variables:
139+
140+
- `TEST_ACCOUNT_NAME` - your GitHub username
141+
- `TEST_ACCOUNT_TYPE` - user
142+
- `TEST_APP_ID` - the App ID of the GitHub App from step 2
143+
- `TEST_CLIENT_ID` - the Client ID of the GitHub App from step 2
144+
- `TEST_INSTALLATION_ID` - the ID of the installation of the GitHub App from step 3
145+
- `TEST_NAME` - the Name of the GitHub App from step 1
146+
- `TEST_WEBHOOK_SECRET` - can be left blank for now
147+
148+
If you are using direnv, there is an `.env.example` file in the repository with all the required environment variables:
149+
150+
```bash
151+
cp .env.example .env
152+
```
153+
154+
Otherwise export the variables in your development environment:
155+
156+
```bash
157+
export TEST_ACCOUNT_NAME="<username>"
158+
# etc...
159+
```
160+
161+
After setup, you can run the test suite with the integration tests enabled by passing the `--integration` argument to any of the test commands:
162+
163+
```bash
164+
uv run nox --session test -- --integration
165+
# or
166+
just test --integration
59167
```

Justfile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ mod docs ".just/documentation.just"
55

66
[private]
77
default:
8-
@just --list
8+
@just --list --list-submodules
9+
10+
[private]
11+
cog:
12+
uv run --with cogapp cog -r README.md
913

1014
[private]
1115
fmt:
@@ -20,8 +24,8 @@ bootstrap:
2024
uv python install
2125
uv sync --locked
2226

23-
coverage:
24-
@just nox coverage
27+
coverage *ARGS:
28+
@just nox coverage {{ ARGS }}
2529

2630
lint:
2731
@just nox lint

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,12 @@ GITHUB_APP = {
390390
391391
Secret used to verify webhook payloads from GitHub.
392392
393+
## Development
394+
395+
For detailed instructions on setting up a development environment and contributing to this project, see [CONTRIBUTING.md](CONTRIBUTING.md).
396+
397+
For release procedures, see [RELEASING.md](RELEASING.md).
398+
393399
## License
394400
395401
django-github-app is licensed under the MIT license. See the [`LICENSE`](LICENSE) file for more information.

noxfile.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,11 @@ def tests(session, django):
8989
session.install(f"django=={django}")
9090

9191
command = ["python", "-m", "pytest"]
92-
if session.posargs and all(arg for arg in session.posargs):
93-
command.append(*session.posargs)
92+
if session.posargs:
93+
args = []
94+
for arg in session.posargs:
95+
args.extend(arg.split(" "))
96+
command.extend(args)
9497
session.run(*command)
9598

9699

@@ -106,7 +109,13 @@ def coverage(session):
106109
)
107110

108111
try:
109-
session.run("python", "-m", "pytest", "--cov", "--cov-report=")
112+
command = ["python", "-m", "pytest", "--cov", "--cov-report="]
113+
if session.posargs:
114+
args = []
115+
for arg in session.posargs:
116+
args.extend(arg.split(" "))
117+
command.extend(args)
118+
session.run(*command)
110119
finally:
111120
# 0 -> OK
112121
# 2 -> code coverage percent unmet

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ dev = [
1010
"ipython>=8.29.0",
1111
"model-bakery>=1.17.0",
1212
"nox[uv]>=2024.10.9",
13+
"pydantic-settings>=2.6.1",
1314
"pytest>=8.3.3",
1415
"pytest-asyncio>=0.24.0",
1516
"pytest-cov>=6.0.0",

src/django_github_app/github.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
8585

8686
class GitHubAPIEndpoint(Enum):
8787
INSTALLATION_REPOS = "/installation/repositories"
88-
ORG_INSTALLATIONS = "/orgs/{org}/installations"
89-
REPO_INSTALLATION = "/repos/{owner}/{repo}/installation"
88+
ORG_APP_INSTALLATION = "/orgs/{org}/installation"
89+
ORG_APP_INSTALLATIONS = "/orgs/{org}/installations"
90+
REPO_APP_INSTALLATION = "/repos/{owner}/{repo}/installation"
91+
REPO_APP_INSTALLATIONS = "/repos/{owner}/{repo}/installations"
9092
REPO_ISSUE = "/repos/{owner}/{repo}/issues/{issue_number}"
9193
REPO_ISSUES = "/repos/{owner}/{repo}/issues"
92-
USER_INSTALLATION = "/users/{username}/installation"
94+
USER_APP_INSTALLATION = "/users/{username}/installation"
95+
USER_APP_INSTALLATIONS = "/users/{username}/installations"
96+
USER_INSTALLATIONS = "/user/installations"
9397

9498

9599
@dataclass(frozen=True, slots=True)

src/django_github_app/management/commands/github.py

Lines changed: 9 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,33 @@
33
from typing import Annotated
44
from typing import Literal
55

6-
from asgiref.sync import async_to_sync
76
from django_typer.management import Typer
87
from typer import Option
98

10-
from django_github_app.conf import app_settings
11-
from django_github_app.github import AsyncGitHubAPI
12-
from django_github_app.github import GitHubAPIEndpoint
13-
from django_github_app.github import GitHubAPIUrl
149
from django_github_app.models import Installation
1510
from django_github_app.models import Repository
1611

1712
cli = Typer(help="Manage your GitHub App")
1813

1914

20-
async def get_installation(name, installation_id, oauth_token):
21-
async with AsyncGitHubAPI(app_settings.SLUG) as gh:
22-
gh.oauth_token = oauth_token
23-
endpoint = GitHubAPIUrl(
24-
GitHubAPIEndpoint.ORG_INSTALLATIONS,
25-
{"org": name},
26-
)
27-
data = await gh.getitem(endpoint.full_url)
28-
for installation in data.get("installations"):
29-
if installation["id"] == installation_id:
30-
return installation
31-
return None
32-
33-
34-
async def get_repos(installation):
35-
async with AsyncGitHubAPI(installation.app_slug) as gh:
36-
gh.oauth_token = await installation.aget_access_token(gh)
37-
url = GitHubAPIUrl(GitHubAPIEndpoint.INSTALLATION_REPOS)
38-
repos = [
39-
repo async for repo in gh.getiter(url.full_url, iterable_key="repositories")
40-
]
41-
print(f"{repos=}")
42-
43-
4415
@cli.command()
4516
def import_app(
46-
name: Annotated[
47-
str,
48-
Option(
49-
help="The name of the user, repository (owner/repo), or organization the GitHub App is installed on"
50-
),
51-
],
5217
type: Annotated[
53-
Literal["user", "repo", "org"],
18+
Literal["user", "org"],
5419
Option(help="The type of account the GitHub App is installed on"),
5520
],
21+
name: Annotated[
22+
str,
23+
Option(help="The user or organization name the GitHub App is installed on"),
24+
],
5625
installation_id: Annotated[
5726
int, Option(help="The installation id of the existing GitHub App")
5827
],
59-
oauth_token: Annotated[
60-
str, Option(help="PAT for accessing GitHub App installations")
61-
],
6228
):
6329
"""
6430
Import an existing GitHub App to database Models.
6531
"""
66-
installation_data = async_to_sync(get_installation)(
67-
name, installation_id, oauth_token
68-
)
69-
if installation_data:
70-
installation = Installation.objects.create_from_gh_data(installation_data)
71-
repository_data = installation.get_repos()
72-
Repository.objects.create_from_gh_data(repository_data)
32+
installation = Installation.objects.create(installation_id=installation_id)
33+
installation.refresh_from_gh(account_type=type, account_name=name)
34+
repository_data = installation.get_repos()
35+
Repository.objects.create_from_gh_data(repository_data, installation)

0 commit comments

Comments
 (0)