read permissions on every GET
This commit is contained in:
@@ -60,11 +60,11 @@ def _set_toggle(value):
|
||||
# Setting ON (default): pair collapses to the primary bay
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_list_collapses_secondary_and_annotates_primary(client, db):
|
||||
def test_list_collapses_secondary_and_annotates_primary(client, db, auth_headers):
|
||||
invalidate_settings_cache() # default on (no row)
|
||||
_seed_dualpath_pair()
|
||||
|
||||
resp = client.get('/api/machines')
|
||||
resp = client.get('/api/machines', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
payload = resp.get_json()
|
||||
items = payload['data']
|
||||
@@ -82,11 +82,11 @@ def test_list_collapses_secondary_and_annotates_primary(client, db):
|
||||
assert partner['machineid'] is not None
|
||||
|
||||
|
||||
def test_dashboard_machine_count_collapses(client, db):
|
||||
def test_dashboard_machine_count_collapses(client, db, auth_headers):
|
||||
invalidate_settings_cache()
|
||||
_seed_dualpath_pair()
|
||||
|
||||
resp = client.get('/api/dashboard')
|
||||
resp = client.get('/api/dashboard', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
counts = resp.get_json()['data']
|
||||
# two machine rows, one physical machine
|
||||
@@ -94,11 +94,11 @@ def test_dashboard_machine_count_collapses(client, db):
|
||||
assert counts['counts']['machines'] == 1
|
||||
|
||||
|
||||
def test_map_excludes_secondary_marker(client, db):
|
||||
def test_map_excludes_secondary_marker(client, db, auth_headers):
|
||||
invalidate_settings_cache()
|
||||
_seed_dualpath_pair()
|
||||
|
||||
resp = client.get('/api/assets/map')
|
||||
resp = client.get('/api/assets/map', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assets = resp.get_json()['data']['assets']
|
||||
numbers = [a['assetnumber'] for a in assets]
|
||||
@@ -109,7 +109,7 @@ def test_map_excludes_secondary_marker(client, db):
|
||||
assert primary['dualpathpartner'] == '2008'
|
||||
|
||||
|
||||
def test_detail_banner_partner_shown_regardless_of_toggle(client, db):
|
||||
def test_detail_banner_partner_shown_regardless_of_toggle(client, db, auth_headers):
|
||||
invalidate_settings_cache()
|
||||
primary_machine, secondary_machine = _seed_dualpath_pair()
|
||||
|
||||
@@ -119,7 +119,7 @@ def test_detail_banner_partner_shown_regardless_of_toggle(client, db):
|
||||
(primary_machine.machineid, '2008'),
|
||||
(secondary_machine.machineid, '2007'),
|
||||
):
|
||||
resp = client.get(f'/api/machines/{machine_id}')
|
||||
resp = client.get(f'/api/machines/{machine_id}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
partner = resp.get_json()['data']['dualpathpartner']
|
||||
assert partner is not None
|
||||
@@ -130,11 +130,11 @@ def test_detail_banner_partner_shown_regardless_of_toggle(client, db):
|
||||
# Setting OFF: both bays list and count separately
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_list_shows_both_bays_when_disabled(client, db):
|
||||
def test_list_shows_both_bays_when_disabled(client, db, auth_headers):
|
||||
_seed_dualpath_pair()
|
||||
_set_toggle(False)
|
||||
|
||||
resp = client.get('/api/machines')
|
||||
resp = client.get('/api/machines', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
payload = resp.get_json()
|
||||
numbers = sorted(i['assetnumber'] for i in payload['data'])
|
||||
@@ -143,20 +143,20 @@ def test_list_shows_both_bays_when_disabled(client, db):
|
||||
assert payload['meta']['pagination']['total'] == 2
|
||||
|
||||
|
||||
def test_dashboard_counts_both_bays_when_disabled(client, db):
|
||||
def test_dashboard_counts_both_bays_when_disabled(client, db, auth_headers):
|
||||
_seed_dualpath_pair()
|
||||
_set_toggle(False)
|
||||
|
||||
resp = client.get('/api/dashboard')
|
||||
resp = client.get('/api/dashboard', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert resp.get_json()['data']['totalmachines'] == 2
|
||||
|
||||
|
||||
def test_map_shows_both_bays_when_disabled(client, db):
|
||||
def test_map_shows_both_bays_when_disabled(client, db, auth_headers):
|
||||
_seed_dualpath_pair()
|
||||
_set_toggle(False)
|
||||
|
||||
resp = client.get('/api/assets/map')
|
||||
resp = client.get('/api/assets/map', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
numbers = [a['assetnumber'] for a in resp.get_json()['data']['assets']]
|
||||
assert '2007' in numbers
|
||||
|
||||
@@ -123,7 +123,7 @@ def test_serve_returns_bytes(client, db, auth_headers):
|
||||
client.post(f'/api/employees/{emp.sso}/photo', data=data,
|
||||
content_type='multipart/form-data', headers=auth_headers)
|
||||
|
||||
served = client.get(f'{EMPLOYEE_PHOTO_URL_PREFIX}photo-{emp.sso}.png')
|
||||
served = client.get(f'{EMPLOYEE_PHOTO_URL_PREFIX}photo-{emp.sso}.png', headers=auth_headers)
|
||||
assert served.status_code == 200
|
||||
assert served.get_data() == payload
|
||||
|
||||
@@ -207,6 +207,6 @@ def test_lookup_includes_photourl(client, db, auth_headers):
|
||||
client.post(f'/api/employees/{emp.sso}/photo', data=data,
|
||||
content_type='multipart/form-data', headers=auth_headers)
|
||||
|
||||
resp = client.get(f'/api/employees/lookup/{emp.sso}')
|
||||
resp = client.get(f'/api/employees/lookup/{emp.sso}', headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()['data']['photourl'] == f'{EMPLOYEE_PHOTO_URL_PREFIX}photo-{emp.sso}.png'
|
||||
|
||||
@@ -22,6 +22,17 @@ from shopdb.core.models import Application
|
||||
from plugins.knowledgebase.models import KnowledgeBase
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _signed_in(client, auth_headers):
|
||||
"""Reading needs a permission now, and these tests are about what the
|
||||
payload says rather than about who may see it. Signing the client in once
|
||||
beats threading headers through every helper in the module."""
|
||||
client.environ_base['HTTP_AUTHORIZATION'] = auth_headers['Authorization']
|
||||
yield
|
||||
client.environ_base.pop('HTTP_AUTHORIZATION', None)
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library(db):
|
||||
"""Articles whose identifying words are deliberately spread across fields."""
|
||||
|
||||
@@ -15,6 +15,17 @@ from shopdb.core.models import Application
|
||||
from plugins.knowledgebase.models import KnowledgeBase
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _signed_in(client, auth_headers):
|
||||
"""Reading needs a permission now, and these tests are about what the
|
||||
payload says rather than about who may see it. Signing the client in once
|
||||
beats threading headers through every helper in the module."""
|
||||
client.environ_base['HTTP_AUTHORIZATION'] = auth_headers['Authorization']
|
||||
yield
|
||||
client.environ_base.pop('HTTP_AUTHORIZATION', None)
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def library(db):
|
||||
"""One application of each interesting shape, with an article each."""
|
||||
@@ -84,7 +95,7 @@ def test_searching_a_title_does_not_resurrect_a_retired_article(client, library)
|
||||
assert 'Live runbook' in found
|
||||
|
||||
|
||||
def test_the_counts_agree_with_the_list(client, library):
|
||||
def test_the_counts_agree_with_the_list(client, library, auth_headers):
|
||||
"""A total that includes articles nobody can see is a total nobody can
|
||||
reconcile against the page."""
|
||||
visible = titles(client)
|
||||
@@ -97,10 +108,11 @@ def test_the_counts_agree_with_the_list(client, library):
|
||||
assert clicks == 6
|
||||
|
||||
|
||||
def test_the_topic_list_offers_every_active_application(client, library):
|
||||
def test_the_topic_list_offers_every_active_application(client, library, auth_headers):
|
||||
"""What the article form's topic dropdown fetches. Installable or not,
|
||||
hidden or not - only retired is excluded."""
|
||||
offered = sorted(row['appname'] for row in client.get(
|
||||
'/api/applications?perpage=100&showhidden=true').get_json()['data'])
|
||||
response = client.get('/api/applications?perpage=100&showhidden=true',
|
||||
headers=auth_headers)
|
||||
offered = sorted(row['appname'] for row in response.get_json()['data'])
|
||||
assert offered == ['Hidden App', 'Live App', 'Manual App']
|
||||
assert 'Retired App' not in offered
|
||||
|
||||
@@ -28,7 +28,7 @@ def _numbers(resp):
|
||||
return sorted(item['assetnumber'] for item in resp.get_json()['data'])
|
||||
|
||||
|
||||
def test_pcs_search_matches_computer_type(client, db):
|
||||
def test_pcs_search_matches_computer_type(client, db, auth_headers):
|
||||
assettype = _assettype('computer')
|
||||
standard = ComputerType(computertype='Standard')
|
||||
_db.session.add(standard)
|
||||
@@ -45,16 +45,16 @@ def test_pcs_search_matches_computer_type(client, db):
|
||||
])
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get('/api/computers?search=Standard')
|
||||
resp = client.get('/api/computers?search=Standard', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['PC-1']
|
||||
|
||||
# the typeless PC is still findable on its own fields
|
||||
resp = client.get('/api/computers?search=bench')
|
||||
resp = client.get('/api/computers?search=bench', headers=auth_headers)
|
||||
assert _numbers(resp) == ['PC-2']
|
||||
|
||||
|
||||
def test_printers_search_matches_type_and_model(client, db):
|
||||
def test_printers_search_matches_type_and_model(client, db, auth_headers):
|
||||
assettype = _assettype('printer')
|
||||
thermal = PrinterType(printertype='Thermal')
|
||||
model = Model(modelnumber='ZT411')
|
||||
@@ -74,15 +74,15 @@ def test_printers_search_matches_type_and_model(client, db):
|
||||
_db.session.commit()
|
||||
|
||||
for term in ['Thermal', 'ZT411']:
|
||||
resp = client.get(f'/api/printers?search={term}')
|
||||
resp = client.get(f'/api/printers?search={term}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['PR-1'], term
|
||||
|
||||
resp = client.get('/api/printers?search=plotter')
|
||||
resp = client.get('/api/printers?search=plotter', headers=auth_headers)
|
||||
assert _numbers(resp) == ['PR-2']
|
||||
|
||||
|
||||
def test_network_search_matches_type_and_vendor(client, db):
|
||||
def test_network_search_matches_type_and_vendor(client, db, auth_headers):
|
||||
assettype = _assettype('network_device')
|
||||
switchtype = NetworkDeviceType(networkdevicetype='Switch')
|
||||
vendor = Vendor(vendor='Cisco')
|
||||
@@ -103,15 +103,15 @@ def test_network_search_matches_type_and_vendor(client, db):
|
||||
_db.session.commit()
|
||||
|
||||
for term in ['Switch', 'Cisco']:
|
||||
resp = client.get(f'/api/network?search={term}')
|
||||
resp = client.get(f'/api/network?search={term}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['NET-1'], term
|
||||
|
||||
resp = client.get('/api/network?search=hub')
|
||||
resp = client.get('/api/network?search=hub', headers=auth_headers)
|
||||
assert _numbers(resp) == ['NET-2']
|
||||
|
||||
|
||||
def test_measuringtools_search_matches_type(client, db):
|
||||
def test_measuringtools_search_matches_type(client, db, auth_headers):
|
||||
assettype = _assettype('measuring_tool')
|
||||
caliper = MeasuringToolType(name='Caliper')
|
||||
_db.session.add(caliper)
|
||||
@@ -129,15 +129,15 @@ def test_measuringtools_search_matches_type(client, db):
|
||||
])
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get('/api/measuringtools?search=Caliper')
|
||||
resp = client.get('/api/measuringtools?search=Caliper', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['MT-1']
|
||||
|
||||
resp = client.get('/api/measuringtools?search=height')
|
||||
resp = client.get('/api/measuringtools?search=height', headers=auth_headers)
|
||||
assert _numbers(resp) == ['MT-2']
|
||||
|
||||
|
||||
def test_core_assets_search_matches_asset_type(client, db):
|
||||
def test_core_assets_search_matches_asset_type(client, db, auth_headers):
|
||||
"""The unified asset list shows a Type column too."""
|
||||
machine_type = _assettype('machine')
|
||||
printer_type = _assettype('printer')
|
||||
@@ -147,12 +147,12 @@ def test_core_assets_search_matches_asset_type(client, db):
|
||||
])
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get('/api/assets?search=machine')
|
||||
resp = client.get('/api/assets?search=machine', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['A-1']
|
||||
|
||||
|
||||
def test_core_assets_search_type_filter_still_works(client, db):
|
||||
def test_core_assets_search_type_filter_still_works(client, db, auth_headers):
|
||||
"""The type-name filter joins AssetType; search uses EXISTS, so both apply."""
|
||||
machine_type = _assettype('machine')
|
||||
_db.session.add_all([
|
||||
@@ -161,6 +161,6 @@ def test_core_assets_search_type_filter_still_works(client, db):
|
||||
])
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get('/api/assets?type=machine&search=mill')
|
||||
resp = client.get('/api/assets?type=machine&search=mill', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['A-1']
|
||||
|
||||
@@ -47,49 +47,49 @@ def _numbers(resp):
|
||||
return sorted(item['assetnumber'] for item in resp.get_json()['data'])
|
||||
|
||||
|
||||
def test_search_matches_machine_type_name(client, db):
|
||||
def test_search_matches_machine_type_name(client, db, auth_headers):
|
||||
_seed_machines()
|
||||
|
||||
resp = client.get('/api/machines?search=Part Washer')
|
||||
resp = client.get('/api/machines?search=Part Washer', headers=auth_headers)
|
||||
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['0410']
|
||||
|
||||
|
||||
def test_search_matches_vendor_name(client, db):
|
||||
def test_search_matches_vendor_name(client, db, auth_headers):
|
||||
_seed_machines()
|
||||
|
||||
resp = client.get('/api/machines?search=Acme')
|
||||
resp = client.get('/api/machines?search=Acme', headers=auth_headers)
|
||||
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['0410']
|
||||
|
||||
|
||||
def test_search_still_matches_asset_fields(client, db):
|
||||
def test_search_still_matches_asset_fields(client, db, auth_headers):
|
||||
_seed_machines()
|
||||
|
||||
for term, expected in [('0999', ['0999']),
|
||||
('Old lathe', ['0999']),
|
||||
('SN-WASH', ['0410'])]:
|
||||
resp = client.get(f'/api/machines?search={term}')
|
||||
resp = client.get(f'/api/machines?search={term}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == expected, term
|
||||
|
||||
|
||||
def test_search_does_not_drop_machines_without_type_or_vendor(client, db):
|
||||
def test_search_does_not_drop_machines_without_type_or_vendor(client, db, auth_headers):
|
||||
"""The outer joins must not turn into inner joins."""
|
||||
_seed_machines()
|
||||
|
||||
resp = client.get('/api/machines?search=lathe')
|
||||
resp = client.get('/api/machines?search=lathe', headers=auth_headers)
|
||||
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == ['0999']
|
||||
|
||||
|
||||
def test_search_miss_returns_empty(client, db):
|
||||
def test_search_miss_returns_empty(client, db, auth_headers):
|
||||
_seed_machines()
|
||||
|
||||
resp = client.get('/api/machines?search=zzzznotathing')
|
||||
resp = client.get('/api/machines?search=zzzznotathing', headers=auth_headers)
|
||||
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert _numbers(resp) == []
|
||||
|
||||
@@ -20,9 +20,20 @@ from shopdb import create_app
|
||||
from shopdb.extensions import db as _db
|
||||
from shopdb.plugins import plugin_manager
|
||||
from plugins.measuringtools.models import (
|
||||
|
||||
derive_status, DUESOON_WINDOW_DAYS, STATUS_COLORS)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _signed_in(client, auth_headers):
|
||||
"""Reading needs a permission now, and these tests are about what the
|
||||
payload says rather than about who may see it. Signing the client in once
|
||||
beats threading headers through every helper in the module."""
|
||||
client.environ_base['HTTP_AUTHORIZATION'] = auth_headers['Authorization']
|
||||
yield
|
||||
client.environ_base.pop('HTTP_AUTHORIZATION', None)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Pure unit tests: derived status (no app needed)
|
||||
# =============================================================================
|
||||
@@ -48,7 +59,7 @@ def test_derive_status_duesoon_on_boundary():
|
||||
assert derive_status(date.today() + timedelta(days=DUESOON_WINDOW_DAYS)) == 'duesoon'
|
||||
|
||||
|
||||
def test_derive_status_duesoon_today():
|
||||
def test_derive_status_duesoon_today(auth_headers):
|
||||
"""Due today is not yet overdue - it falls in the due-soon window."""
|
||||
assert derive_status(date.today()) == 'duesoon'
|
||||
|
||||
@@ -120,14 +131,17 @@ def _status_id(client):
|
||||
|
||||
# -- Type CRUD + in-use guard -------------------------------------------------
|
||||
|
||||
def test_starter_types_seeded(client):
|
||||
response = client.get('/api/measuringtools/types')
|
||||
def test_starter_types_seeded(client, auth_headers):
|
||||
response = client.get('/api/measuringtools/types', headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
names = {t['name'] for t in response.get_json()['data']}
|
||||
assert {'Caliper', 'Micrometer', 'Thread Gage', 'Other'} <= names
|
||||
|
||||
|
||||
def test_type_create_requires_auth(client):
|
||||
# This module signs the client in for every test; this one is about what
|
||||
# happens when nobody is signed in, so it takes that back.
|
||||
client.environ_base.pop('HTTP_AUTHORIZATION', None)
|
||||
response = client.post('/api/measuringtools/types', json={'name': 'Nope'})
|
||||
assert response.status_code == 401
|
||||
|
||||
@@ -197,7 +211,7 @@ def test_tool_create_and_get_merged(client, auth_headers):
|
||||
assert ext['calibrationstatus'] == 'current'
|
||||
tool_id = ext['measuringtoolid']
|
||||
|
||||
fetched = client.get(f'/api/measuringtools/{tool_id}')
|
||||
fetched = client.get(f'/api/measuringtools/{tool_id}', headers=auth_headers)
|
||||
assert fetched.status_code == 200
|
||||
assert fetched.get_json()['data']['measuringtool']['measuringtoolid'] == tool_id
|
||||
|
||||
@@ -263,7 +277,7 @@ def test_list_filter_by_calibrationstatus(client, auth_headers):
|
||||
'assetnumber': 'MT-OVERDUE-1', 'statusid': _status_id(client),
|
||||
'nextcalibrationdate': str(date.today() - timedelta(days=10)),
|
||||
})
|
||||
response = client.get('/api/measuringtools?calibrationstatus=overdue')
|
||||
response = client.get('/api/measuringtools?calibrationstatus=overdue', headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
rows = response.get_json()['data']
|
||||
assert rows, 'expected at least one overdue tool'
|
||||
@@ -341,7 +355,7 @@ def test_search_excludes_tools_when_domain_disabled(mt_app, client, auth_headers
|
||||
# -- Report shape -------------------------------------------------------------
|
||||
|
||||
def test_calibration_report_shape(client, auth_headers):
|
||||
response = client.get('/api/measuringtools/report/calibration')
|
||||
response = client.get('/api/measuringtools/report/calibration', headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()['data']
|
||||
assert set(data['counts']) == {'overdue', 'duesoon', 'current', 'unknown'}
|
||||
@@ -382,7 +396,7 @@ def test_asset_todict_carries_measuringtool_typedata_and_pluginid(mt_app, client
|
||||
|
||||
def test_map_lists_measuringtool_subtypes(client, auth_headers):
|
||||
"""The map filter dropdown carries a Measuring Tool subtype list with color."""
|
||||
response = client.get('/api/assets/map')
|
||||
response = client.get('/api/assets/map', headers=auth_headers)
|
||||
assert response.status_code == 200, response.get_json()
|
||||
subtypes = response.get_json()['data']['filters']['subtypes']
|
||||
assert 'Measuring Tool' in subtypes
|
||||
@@ -404,7 +418,7 @@ def test_map_honors_measuringtool_subtype_filter(client, auth_headers):
|
||||
'measuringtooltypeid': micrometer_id, 'mapx': 30, 'mapy': 40})
|
||||
|
||||
response = client.get(
|
||||
f'/api/assets/map?assettype=measuring_tool&subtype={caliper_id}')
|
||||
f'/api/assets/map?assettype=measuring_tool&subtype={caliper_id}', headers=auth_headers)
|
||||
assert response.status_code == 200, response.get_json()
|
||||
numbers = {a['assetnumber'] for a in response.get_json()['data']['assets']}
|
||||
assert 'MT-MAP-CAL' in numbers
|
||||
@@ -416,7 +430,7 @@ def test_map_item_carries_measuringtool_typedata(client, auth_headers):
|
||||
client.post('/api/measuringtools', headers=auth_headers, json={
|
||||
'assetnumber': 'MT-MAP-TD', 'statusid': _status_id(client),
|
||||
'measuringtooltypeid': _caliper_id(client), 'mapx': 55, 'mapy': 66})
|
||||
response = client.get('/api/assets/map?assettype=measuring_tool')
|
||||
response = client.get('/api/assets/map?assettype=measuring_tool', headers=auth_headers)
|
||||
item = next(a for a in response.get_json()['data']['assets']
|
||||
if a['assetnumber'] == 'MT-MAP-TD')
|
||||
assert item['typedata']['measuringtooltypename'] == 'Caliper'
|
||||
@@ -444,7 +458,7 @@ def test_map_overlay_shape_and_derivation(client, auth_headers):
|
||||
'nextcalibrationdate': str(date.today() - timedelta(days=3))}) # overdue
|
||||
assetid = created.get_json()['data']['assetid']
|
||||
|
||||
response = client.get('/api/measuringtools/map-overlay')
|
||||
response = client.get('/api/measuringtools/map-overlay', headers=auth_headers)
|
||||
assert response.status_code == 200, response.get_json()
|
||||
rows = response.get_json()['data']
|
||||
# ADR-010 overlay contract: [{assetid, color, label}], only due/overdue tools.
|
||||
|
||||
@@ -39,55 +39,55 @@ def _seed_model(imageurl=IMAGE_URL):
|
||||
return model
|
||||
|
||||
|
||||
def test_machine_detail_carries_model_imageurl(client, db):
|
||||
def test_machine_detail_carries_model_imageurl(client, db, auth_headers):
|
||||
asset = _seed_asset('2001', 'machine')
|
||||
model = _seed_model()
|
||||
machine = Machine(assetid=asset.assetid, modelnumberid=model.modelnumberid)
|
||||
_db.session.add(machine)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/machines/{machine.machineid}')
|
||||
resp = client.get(f'/api/machines/{machine.machineid}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert resp.get_json()['data']['machine']['imageurl'] == IMAGE_URL
|
||||
|
||||
|
||||
def test_computer_detail_carries_model_imageurl(client, db):
|
||||
def test_computer_detail_carries_model_imageurl(client, db, auth_headers):
|
||||
asset = _seed_asset('PC-2001', 'computer')
|
||||
model = _seed_model()
|
||||
computer = Computer(assetid=asset.assetid, modelnumberid=model.modelnumberid)
|
||||
_db.session.add(computer)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/computers/{computer.computerid}')
|
||||
resp = client.get(f'/api/computers/{computer.computerid}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert resp.get_json()['data']['computer']['imageurl'] == IMAGE_URL
|
||||
|
||||
|
||||
def test_printer_detail_carries_model_imageurl(client, db):
|
||||
def test_printer_detail_carries_model_imageurl(client, db, auth_headers):
|
||||
asset = _seed_asset('PR-2001', 'printer')
|
||||
model = _seed_model()
|
||||
printer = Printer(assetid=asset.assetid, modelnumberid=model.modelnumberid)
|
||||
_db.session.add(printer)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/printers/{printer.printerid}')
|
||||
resp = client.get(f'/api/printers/{printer.printerid}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert resp.get_json()['data']['printer']['imageurl'] == IMAGE_URL
|
||||
|
||||
|
||||
def test_detail_omits_imageurl_without_model(client, db):
|
||||
def test_detail_omits_imageurl_without_model(client, db, auth_headers):
|
||||
"""No model linked -> no imageurl key (frontend v-if shows nothing)."""
|
||||
asset = _seed_asset('PR-2002', 'printer')
|
||||
printer = Printer(assetid=asset.assetid)
|
||||
_db.session.add(printer)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/printers/{printer.printerid}')
|
||||
resp = client.get(f'/api/printers/{printer.printerid}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert 'imageurl' not in resp.get_json()['data']['printer']
|
||||
|
||||
|
||||
def test_detail_omits_imageurl_when_model_has_no_image(client, db):
|
||||
def test_detail_omits_imageurl_when_model_has_no_image(client, db, auth_headers):
|
||||
"""Model linked but no photo -> no imageurl key."""
|
||||
asset = _seed_asset('PC-2002', 'computer')
|
||||
model = _seed_model(imageurl=None)
|
||||
@@ -95,6 +95,6 @@ def test_detail_omits_imageurl_when_model_has_no_image(client, db):
|
||||
_db.session.add(computer)
|
||||
_db.session.commit()
|
||||
|
||||
resp = client.get(f'/api/computers/{computer.computerid}')
|
||||
resp = client.get(f'/api/computers/{computer.computerid}', headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert 'imageurl' not in resp.get_json()['data']['computer']
|
||||
|
||||
@@ -18,8 +18,8 @@ def model(db):
|
||||
return model
|
||||
|
||||
|
||||
def test_supplies_meta_lists_allowed_values(client, db):
|
||||
response = client.get('/api/printers/supplies/meta')
|
||||
def test_supplies_meta_lists_allowed_values(client, db, auth_headers):
|
||||
response = client.get('/api/printers/supplies/meta', headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()['data']
|
||||
assert 'toner' in data['supplytypes']
|
||||
@@ -38,7 +38,7 @@ def test_create_and_list_model_supply(client, model, auth_headers):
|
||||
)
|
||||
assert create.status_code == 201
|
||||
|
||||
listing = client.get(f'/api/printers/models/{model.modelnumberid}/supplies')
|
||||
listing = client.get(f'/api/printers/models/{model.modelnumberid}/supplies', headers=auth_headers)
|
||||
assert listing.status_code == 200
|
||||
supplies = listing.get_json()['data']['supplies']
|
||||
assert len(supplies) == 1
|
||||
@@ -90,7 +90,7 @@ def test_update_and_delete_supply(client, model, auth_headers):
|
||||
deleted = client.delete(f'/api/printers/supplies/{supplyid}', headers=auth_headers)
|
||||
assert deleted.status_code == 200
|
||||
|
||||
listing = client.get(f'/api/printers/models/{model.modelnumberid}/supplies')
|
||||
listing = client.get(f'/api/printers/models/{model.modelnumberid}/supplies', headers=auth_headers)
|
||||
assert listing.get_json()['data']['supplies'] == []
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ def test_listmodels_reports_supplycount(client, model, auth_headers):
|
||||
client.post(f'/api/printers/models/{model.modelnumberid}/supplies',
|
||||
json={'partnumber': 'W2020A', 'color': 'black'}, headers=auth_headers)
|
||||
|
||||
response = client.get('/api/printers/models', query_string={'search': 'C999'})
|
||||
response = client.get('/api/printers/models', query_string={'search': 'C999'}, headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
rows = response.get_json()['data']
|
||||
match = next(r for r in rows if r['modelnumberid'] == model.modelnumberid)
|
||||
|
||||
@@ -148,20 +148,20 @@ def test_kiosk_take_is_open_decrement_only(client, auth_headers, app, item,
|
||||
headers=auth_headers)
|
||||
assert stocked.status_code == 200
|
||||
|
||||
lookup = client.get(f'/api/printedparts/kiosk/item/{itemcode}')
|
||||
lookup = client.get(f'/api/printedparts/kiosk/item/{itemcode}', headers=auth_headers)
|
||||
assert lookup.status_code == 200
|
||||
|
||||
take = client.post('/api/printedparts/kiosk/take', json={
|
||||
'itemcode': itemcode, 'badge': directory_employee, 'quantity': 2})
|
||||
'itemcode': itemcode, 'badge': directory_employee, 'quantity': 2}, headers=auth_headers)
|
||||
assert take.status_code == 200, take.get_json()
|
||||
assert take.get_json()['data']['quantityonhand'] == 3
|
||||
|
||||
too_many = client.post('/api/printedparts/kiosk/take', json={
|
||||
'itemcode': itemcode, 'badge': directory_employee, 'quantity': 99})
|
||||
'itemcode': itemcode, 'badge': directory_employee, 'quantity': 99}, headers=auth_headers)
|
||||
assert too_many.status_code == 400
|
||||
|
||||
unknown = client.post('/api/printedparts/kiosk/take', json={
|
||||
'itemcode': itemcode, 'badge': '111111111', 'quantity': 1})
|
||||
'itemcode': itemcode, 'badge': '111111111', 'quantity': 1}, headers=auth_headers)
|
||||
assert unknown.status_code == 422
|
||||
|
||||
with app.app_context():
|
||||
@@ -197,7 +197,7 @@ def test_lowstock_alert_fires_on_crossing_only(client, auth_headers, app, item,
|
||||
return client.post('/api/printedparts/kiosk/take',
|
||||
json={'itemcode': '3DP-9001',
|
||||
'badge': directory_employee,
|
||||
'quantity': quantity})
|
||||
'quantity': quantity}, headers=auth_headers)
|
||||
|
||||
restock(10) # 10 on hand, threshold 5
|
||||
assert take(3).status_code == 200 # 7: above threshold, no alert
|
||||
@@ -245,7 +245,7 @@ def test_alert_recipients_merge_users_and_freetext(client, auth_headers, app,
|
||||
headers=auth_headers)
|
||||
take = client.post('/api/printedparts/kiosk/take',
|
||||
json={'itemcode': '3DP-9001',
|
||||
'badge': directory_employee, 'quantity': 6})
|
||||
'badge': directory_employee, 'quantity': 6}, headers=auth_headers)
|
||||
assert take.status_code == 200 # 4 on hand: crossed threshold 5
|
||||
|
||||
assert captured['to'] == ['lead@site.test', 'extra@site.test']
|
||||
@@ -260,7 +260,7 @@ def test_retire_hides_and_restore_returns(client, auth_headers, item):
|
||||
listed = client.get('/api/printedparts/items',
|
||||
headers=auth_headers).get_json()['data']
|
||||
assert all(row['printeditemid'] != item for row in listed)
|
||||
kiosk = client.get('/api/printedparts/kiosk/item/3DP-9001')
|
||||
kiosk = client.get('/api/printedparts/kiosk/item/3DP-9001', headers=auth_headers)
|
||||
assert kiosk.status_code == 404
|
||||
|
||||
including = client.get('/api/printedparts/items?active=false',
|
||||
@@ -270,7 +270,7 @@ def test_retire_hides_and_restore_returns(client, auth_headers, item):
|
||||
|
||||
assert client.post(f'/api/printedparts/items/{item}/restore',
|
||||
headers=auth_headers).status_code == 200
|
||||
assert client.get('/api/printedparts/kiosk/item/3DP-9001').status_code == 200
|
||||
assert client.get('/api/printedparts/kiosk/item/3DP-9001', headers=auth_headers).status_code == 200
|
||||
|
||||
|
||||
def test_file_revisions_append_and_download(client, auth_headers, item, tmp_path):
|
||||
@@ -302,7 +302,7 @@ def test_file_revisions_append_and_download(client, auth_headers, item, tmp_path
|
||||
assert [f['revision'] for f in listing] == [2, 1]
|
||||
|
||||
fileid = listing[1]['fileid']
|
||||
download = client.get(f'/api/printedparts/files/{fileid}/download')
|
||||
download = client.get(f'/api/printedparts/files/{fileid}/download', headers=auth_headers)
|
||||
assert download.status_code == 200
|
||||
assert download.data == b'solid part'
|
||||
assert 'clip_v1.stl' in download.headers['Content-Disposition']
|
||||
@@ -335,7 +335,7 @@ def test_alert_role_members_receive(client, auth_headers, app, item,
|
||||
headers=auth_headers)
|
||||
take = client.post('/api/printedparts/kiosk/take',
|
||||
json={'itemcode': '3DP-9001',
|
||||
'badge': directory_employee, 'quantity': 6})
|
||||
'badge': directory_employee, 'quantity': 6}, headers=auth_headers)
|
||||
assert take.status_code == 200
|
||||
assert captured['to'] == ['crewone@site.test']
|
||||
|
||||
@@ -362,14 +362,14 @@ def test_gagelabtag_assigned_searched_and_kiosk_resolved(client, auth_headers):
|
||||
headers=auth_headers).get_json()['data']
|
||||
assert len(searched) == 1
|
||||
|
||||
by_tag = client.get('/api/printedparts/kiosk/item/WJRP0117')
|
||||
by_tag = client.get('/api/printedparts/kiosk/item/WJRP0117', headers=auth_headers)
|
||||
assert by_tag.status_code == 200
|
||||
by_digits = client.get('/api/printedparts/kiosk/item/117')
|
||||
by_digits = client.get('/api/printedparts/kiosk/item/117', headers=auth_headers)
|
||||
assert by_digits.status_code == 200
|
||||
assert by_digits.get_json()['data']['gagelabtag'] == 'WJRP0117'
|
||||
|
||||
|
||||
def test_kiosk_take_records_revision_from_qr(client, app, db, directory_employee):
|
||||
def test_kiosk_take_records_revision_from_qr(client, app, db, directory_employee, auth_headers):
|
||||
"""A label QR payload 'TAG|rev' resolves the item by TAG and records the
|
||||
revision on the take (traceability of which print-file rev was consumed)."""
|
||||
with app.app_context():
|
||||
@@ -380,7 +380,7 @@ def test_kiosk_take_records_revision_from_qr(client, app, db, directory_employee
|
||||
iid = row.printeditemid
|
||||
|
||||
take = client.post('/api/printedparts/kiosk/take', json={
|
||||
'itemcode': 'WJRP7000|3', 'badge': directory_employee, 'quantity': 1})
|
||||
'itemcode': 'WJRP7000|3', 'badge': directory_employee, 'quantity': 1}, headers=auth_headers)
|
||||
assert take.status_code == 200, take.get_json()
|
||||
|
||||
with app.app_context():
|
||||
@@ -391,7 +391,7 @@ def test_kiosk_take_records_revision_from_qr(client, app, db, directory_employee
|
||||
|
||||
# Manual entry (no |rev) records a null revision, still resolves.
|
||||
take2 = client.post('/api/printedparts/kiosk/take', json={
|
||||
'itemcode': 'WJRP7000', 'badge': directory_employee, 'quantity': 1})
|
||||
'itemcode': 'WJRP7000', 'badge': directory_employee, 'quantity': 1}, headers=auth_headers)
|
||||
assert take2.status_code == 200
|
||||
with app.app_context():
|
||||
last = (PrintedItemTransaction.query
|
||||
@@ -400,7 +400,7 @@ def test_kiosk_take_records_revision_from_qr(client, app, db, directory_employee
|
||||
assert last.revision is None
|
||||
|
||||
|
||||
def test_lowstock_routes_to_support_team_webhook(client, app, db, directory_employee):
|
||||
def test_lowstock_routes_to_support_team_webhook(client, app, db, directory_employee, auth_headers):
|
||||
"""A low-stock crossing posts to the selected support team's webhook URL."""
|
||||
from unittest.mock import patch
|
||||
from shopdb.core.models import SupportTeam, Setting
|
||||
@@ -418,7 +418,7 @@ def test_lowstock_routes_to_support_team_webhook(client, app, db, directory_empl
|
||||
with patch('shopdb.api.send_webhook') as mock_send:
|
||||
# take 2 -> 6 crosses to 4 (<= threshold 5) -> alert fires
|
||||
resp = client.post('/api/printedparts/kiosk/take', json={
|
||||
'itemcode': 'WJRP7100', 'badge': directory_employee, 'quantity': 2})
|
||||
'itemcode': 'WJRP7100', 'badge': directory_employee, 'quantity': 2}, headers=auth_headers)
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
assert mock_send.called
|
||||
assert mock_send.call_args.kwargs['url'] == 'https://teams.example/3dprint'
|
||||
|
||||
@@ -299,10 +299,10 @@ def test_no_estimate_never_reaches_the_order_list():
|
||||
assert orderlist([cartridge(None)]) == []
|
||||
|
||||
|
||||
def test_forecast_endpoint_says_so_when_zabbix_is_absent(client, db):
|
||||
def test_forecast_endpoint_says_so_when_zabbix_is_absent(client, db, auth_headers):
|
||||
"""A dashboard must be told the data is missing, not shown an empty list
|
||||
that reads as 'nothing runs out soon'."""
|
||||
response = client.get('/api/printers/supplies/forecast')
|
||||
response = client.get('/api/printers/supplies/forecast', headers=auth_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.get_json()['data']
|
||||
|
||||
@@ -14,6 +14,17 @@ from shopdb.core.models import Model, Vendor
|
||||
from plugins.printers.models import ModelSupply, Printer
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _signed_in(client, auth_headers):
|
||||
"""Reading needs a permission now, and these tests are about what the
|
||||
payload says rather than about who may see it. Signing the client in once
|
||||
beats threading headers through every helper in the module."""
|
||||
client.environ_base['HTTP_AUTHORIZATION'] = auth_headers['Authorization']
|
||||
yield
|
||||
client.environ_base.pop('HTTP_AUTHORIZATION', None)
|
||||
|
||||
|
||||
|
||||
LEVELS = {
|
||||
# itemid: (name, live level, history of daily readings)
|
||||
# Black is a REAL zero, which is the only thing that means empty. Magenta is
|
||||
|
||||
Reference in New Issue
Block a user