Skip to content

Commit 8e6ab95

Browse files
committed
Fix test suite imports and add CI lint + PyPI release workflow
- conftest.py: mock utilities.htmx and provide a real _FakeBaseTable (subclass of django_tables2.Table) so views.py imports cleanly and CustomObjectsTabTable can be subclassed in tests - test_register_tabs.py: add TestCustomObjectsTabTable (6 tests) and TestSortHeader (4 tests); all 24 tests now pass - ci.yml: add ruff lint job (check + format --check) before the test job - release.yml: new workflow — builds sdist/wheel and publishes to PyPI via OIDC Trusted Publishing when a GitHub Release is published
1 parent 57db3b8 commit 8e6ab95

4 files changed

Lines changed: 168 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ on:
77
branches: [main, master]
88

99
jobs:
10+
lint:
11+
name: Lint
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.12"
18+
- run: pip install ruff
19+
- run: ruff check netbox_custom_objects_tab/
20+
- run: ruff format --check netbox_custom_objects_tab/
21+
1022
test:
1123
name: Run tests
1224
runs-on: ubuntu-latest

.github/workflows/release.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
build:
9+
name: Build distribution
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v5
14+
with:
15+
python-version: "3.12"
16+
- run: pip install build
17+
- run: python -m build
18+
- uses: actions/upload-artifact@v4
19+
with:
20+
name: dist
21+
path: dist/
22+
23+
publish:
24+
name: Publish to PyPI
25+
needs: build
26+
runs-on: ubuntu-latest
27+
environment: pypi
28+
permissions:
29+
id-token: write
30+
steps:
31+
- uses: actions/download-artifact@v4
32+
with:
33+
name: dist
34+
path: dist/
35+
- uses: pypa/gh-action-pypi-publish@release/v1

tests/conftest.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from types import ModuleType
1010
from unittest.mock import MagicMock
1111

12+
import django_tables2 as _tables2
13+
1214

1315
def _mock(dotted_name, **attrs):
1416
"""
@@ -62,6 +64,47 @@ class _CustomFieldTypeChoices:
6264
_mock('utilities')
6365
_mock('utilities.views', ViewTab=MagicMock(), register_model_view=MagicMock())
6466
_mock('utilities.paginator', EnhancedPaginator=MagicMock(), get_paginate_count=MagicMock())
67+
_mock('utilities.htmx', htmx_partial=MagicMock())
68+
69+
70+
class _FakeBaseTable(_tables2.Table):
71+
exempt_columns = ()
72+
73+
class Meta:
74+
attrs = {}
75+
76+
@property
77+
def name(self):
78+
return self.__class__.__name__
79+
80+
def _get_columns(self, visible=True):
81+
return [
82+
(name, col.verbose_name)
83+
for name, col in self.columns.items()
84+
if col.visible == visible and name not in self.exempt_columns
85+
]
86+
87+
@property
88+
def available_columns(self):
89+
return sorted(self._get_columns(visible=False))
90+
91+
@property
92+
def selected_columns(self):
93+
return self._get_columns(visible=True)
94+
95+
def _set_columns(self, selected_columns):
96+
for name, column in self.columns.items():
97+
if column.name not in [*selected_columns, *self.exempt_columns]:
98+
self.columns.hide(column.name)
99+
else:
100+
self.columns.show(column.name)
101+
self.sequence = [
102+
*[c for c in selected_columns if c in self.columns.names()],
103+
*[c for c in self.columns.names() if c not in selected_columns],
104+
]
105+
106+
107+
_mock('netbox.tables', BaseTable=_FakeBaseTable)
65108

66109
# --- netbox_custom_objects.* ---
67110
_mock('netbox_custom_objects')

tests/test_register_tabs.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,81 @@ def test_unknown_specific_model_logs_warning_no_exception(self, caplog):
201201
views.register_tabs() # must not raise
202202

203203
assert any('nonexistent_app_xyz.somemodel' in r.message for r in caplog.records)
204+
205+
206+
# ---------------------------------------------------------------------------
207+
# CustomObjectsTabTable
208+
# ---------------------------------------------------------------------------
209+
210+
class TestCustomObjectsTabTable:
211+
"""Column-preference machinery on the lightweight table class."""
212+
213+
@pytest.fixture(autouse=True)
214+
def table_cls(self):
215+
from netbox_custom_objects_tab.views import CustomObjectsTabTable
216+
self.cls = CustomObjectsTabTable
217+
218+
def test_default_columns_contains_all_six(self):
219+
assert set(self.cls.Meta.default_columns) == {
220+
'type', 'object', 'value', 'field', 'tags', 'actions'
221+
}
222+
223+
def test_actions_is_exempt(self):
224+
assert 'actions' in self.cls.exempt_columns
225+
226+
def test_name_property(self):
227+
t = self.cls([], empty_text='')
228+
assert t.name == 'CustomObjectsTabTable'
229+
230+
def test_all_columns_visible_by_default(self):
231+
t = self.cls([], empty_text='')
232+
t._set_columns(list(self.cls.Meta.default_columns))
233+
visible = {col for col, _ in t.selected_columns}
234+
assert {'type', 'object', 'value', 'field', 'tags'}.issubset(visible)
235+
236+
def test_hidden_column_not_in_selected(self):
237+
t = self.cls([], empty_text='')
238+
cols_without_value = [c for c in self.cls.Meta.default_columns if c != 'value']
239+
t._set_columns(cols_without_value)
240+
visible = {col for col, _ in t.selected_columns}
241+
assert 'value' not in visible
242+
243+
def test_exempt_column_always_visible(self):
244+
t = self.cls([], empty_text='')
245+
# Pass only non-exempt, non-actions columns
246+
t._set_columns(['type'])
247+
# 'actions' is exempt — must not appear in selected_columns
248+
# (exempt columns are excluded from the modal, not from rendering)
249+
selected_names = {col for col, _ in t.selected_columns}
250+
assert 'actions' not in selected_names # exempt cols excluded from selected_columns
251+
252+
253+
# ---------------------------------------------------------------------------
254+
# _sort_header
255+
# ---------------------------------------------------------------------------
256+
257+
class TestSortHeader:
258+
@pytest.fixture(autouse=True)
259+
def get_fn(self):
260+
from netbox_custom_objects_tab.views import _sort_header
261+
self.fn = _sort_header
262+
263+
def test_inactive_column_points_to_asc(self):
264+
result = self.fn('', 'type', 'object', 'asc')
265+
assert 'sort=type' in result['url']
266+
assert 'dir=asc' in result['url']
267+
assert result['icon'] is None
268+
269+
def test_active_asc_column_icon_is_arrow_up(self):
270+
result = self.fn('', 'type', 'type', 'asc')
271+
assert result['icon'] == 'arrow-up'
272+
assert 'dir=desc' in result['url']
273+
274+
def test_active_desc_column_icon_is_arrow_down(self):
275+
result = self.fn('', 'type', 'type', 'desc')
276+
assert result['icon'] == 'arrow-down'
277+
assert 'dir=asc' in result['url']
278+
279+
def test_base_params_preserved(self):
280+
result = self.fn('q=foo&tag=bar', 'type', '', 'asc')
281+
assert result['url'].startswith('?q=foo&tag=bar&')

0 commit comments

Comments
 (0)