117 lines
6.6 KiB
Python
117 lines
6.6 KiB
Python
|
|
"""Gateway inventory data must survive reviewed PC/Mac asset creation."""
|
||
|
|
import asyncio
|
||
|
|
import json
|
||
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
|
import pytest
|
||
|
|
from fastapi import HTTPException
|
||
|
|
from app.modules.hardware.backend import inventory
|
||
|
|
|
||
|
|
|
||
|
|
def test_windows_list_and_mac_detail_formats():
|
||
|
|
raw = {'hardware': {'manufacturer':'Apple','model':'Mac mini','cpu':{'name':'M4'},'ram_gb':24}, 'os':{'caption':'macOS'}}
|
||
|
|
for field, value in [('hardware_data',json.dumps(raw)), ('json_data',raw)]:
|
||
|
|
item = inventory.inventory_item({'id':12,'computer_name':'MAC-12','serial_number':'SN12',field:value})
|
||
|
|
assert (item['brand'],item['model'],item['cpu'],item['ram_gb'],item['os']) == ('Apple','Mac mini','M4',24,'macOS')
|
||
|
|
assert item['hardware_specs']['gateway_inventory']['data'] == raw
|
||
|
|
assert item['serial_number'] == 'SN12'
|
||
|
|
|
||
|
|
|
||
|
|
def response_session(status, payload):
|
||
|
|
response = MagicMock(status=status)
|
||
|
|
response.json = AsyncMock(return_value=payload)
|
||
|
|
context = MagicMock()
|
||
|
|
context.__aenter__ = AsyncMock(return_value=response)
|
||
|
|
session = MagicMock()
|
||
|
|
session.get.return_value = context
|
||
|
|
client = MagicMock()
|
||
|
|
client.__aenter__ = AsyncMock(return_value=session)
|
||
|
|
return client, session
|
||
|
|
|
||
|
|
|
||
|
|
def test_search_uses_gateway_filter_and_pagination():
|
||
|
|
client, session = response_session(200, {'data':[{'id':1,'computer_name':'PC'}],'pagination':{'total_pages':3}})
|
||
|
|
with patch.object(inventory,'_apigw_headers',return_value={'Authorization':'Bearer fixture'}), patch.object(inventory.aiohttp,'ClientSession',return_value=client):
|
||
|
|
result = asyncio.run(inventory.search_inventory('PC', 'name', 2))
|
||
|
|
assert result['items'][0]['name'] == 'PC'
|
||
|
|
assert result['pagination']['total_pages'] == 3
|
||
|
|
assert session.get.call_args.kwargs['params'] == {'computer_name':'PC','per_page':20,'page':2}
|
||
|
|
|
||
|
|
|
||
|
|
def test_gateway_failure_is_sanitized_and_serial_not_found_is_empty():
|
||
|
|
for status, by in [(403,'name'), (404,'serial')]:
|
||
|
|
client, session = response_session(status, {'error':'private upstream data'})
|
||
|
|
with patch.object(inventory,'_apigw_headers',return_value={}), patch.object(inventory.aiohttp,'ClientSession',return_value=client), patch.object(inventory,'find_inventory_in_list',AsyncMock(return_value=None)):
|
||
|
|
if by == 'serial':
|
||
|
|
assert asyncio.run(inventory.search_inventory('SN/12', by, 1))['items'] == []
|
||
|
|
assert session.get.call_args.args[0].endswith('/serial/SN%2F12')
|
||
|
|
else:
|
||
|
|
with pytest.raises(HTTPException) as error:
|
||
|
|
asyncio.run(inventory.search_inventory('', by, 1))
|
||
|
|
assert error.value.status_code == 502
|
||
|
|
assert 'private' not in error.value.detail
|
||
|
|
|
||
|
|
|
||
|
|
def test_pair_fetches_trusted_gateway_record_and_merges_only_inventory():
|
||
|
|
item = inventory.inventory_item({'id':12,'computer_name':'PC12','json_data':{'hardware':{'ram_gb':16}}})
|
||
|
|
with patch.object(inventory,'fetch_inventory_record',AsyncMock(return_value=item)) as fetch, patch.object(inventory,'execute_query',side_effect=[[{'id':4}],[{'id':4}]]) as query:
|
||
|
|
result = asyncio.run(inventory.pair_inventory(4,inventory.InventoryPairInput(inventory_id=12)))
|
||
|
|
fetch.assert_awaited_once_with(12)
|
||
|
|
sql, params = query.call_args.args
|
||
|
|
assert "COALESCE(hardware_specs, '{}'::jsonb) ||" in sql
|
||
|
|
assert params[0].adapted == item['hardware_specs']
|
||
|
|
assert set(params[0].adapted) == {'gateway_inventory'}
|
||
|
|
assert 'serial_number =' not in sql
|
||
|
|
assert 'current_owner' not in sql
|
||
|
|
assert result['hardware_id'] == 4
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_asset_and_gateway_failure_never_write():
|
||
|
|
with patch.object(inventory,'execute_query',return_value=[]) as query, patch.object(inventory,'fetch_inventory_record',AsyncMock()) as fetch:
|
||
|
|
with pytest.raises(HTTPException) as error:
|
||
|
|
asyncio.run(inventory.pair_inventory(4,inventory.InventoryPairInput(inventory_id=12)))
|
||
|
|
assert error.value.status_code == 404
|
||
|
|
fetch.assert_not_awaited()
|
||
|
|
assert query.call_count == 1
|
||
|
|
with patch.object(inventory,'execute_query',return_value=[{'id':4}]) as query, patch.object(inventory,'fetch_inventory_record',AsyncMock(side_effect=HTTPException(502,'Unavailable'))):
|
||
|
|
with pytest.raises(HTTPException):
|
||
|
|
asyncio.run(inventory.pair_inventory(4,inventory.InventoryPairInput(inventory_id=12)))
|
||
|
|
assert query.call_count == 1
|
||
|
|
|
||
|
|
|
||
|
|
def test_unpair_removes_only_inventory_key():
|
||
|
|
with patch.object(inventory,'execute_query',return_value=[{'id':4}]) as query:
|
||
|
|
assert asyncio.run(inventory.unpair_inventory(4))['unpaired'] is True
|
||
|
|
assert "- 'gateway_inventory'" in query.call_args.args[0]
|
||
|
|
assert query.call_args.args[1] == (4,)
|
||
|
|
|
||
|
|
|
||
|
|
def test_detail_fetch_normalizes_gateway_record():
|
||
|
|
client, session = response_session(200, {'id':12,'computer_name':'Mac','json_data':{'os':{'caption':'macOS'}}})
|
||
|
|
with patch.object(inventory,'_apigw_headers',return_value={}), patch.object(inventory.aiohttp,'ClientSession',return_value=client):
|
||
|
|
result = asyncio.run(inventory.fetch_inventory_record(12))
|
||
|
|
assert session.get.call_args.args[0].endswith('/api/inventory/12')
|
||
|
|
assert result['os'] == 'macOS'
|
||
|
|
|
||
|
|
|
||
|
|
def test_pair_detail_404_falls_back_to_fresh_paginated_list():
|
||
|
|
client, session = response_session(404, {})
|
||
|
|
def context(status, payload):
|
||
|
|
return response_session(status, payload)[0].__aenter__.return_value.get.return_value
|
||
|
|
session.get.side_effect = [context(404, {}), context(200, {'data':[{'id':1}], 'pagination':{'total_pages':2}}), context(200, {'data':[{'id':'12','computer_name':'Mac','hardware_data':'{"hardware":{"ram_gb":24}}'}], 'pagination':{'total_pages':2}})]
|
||
|
|
with patch.object(inventory,'_apigw_headers',return_value={}), patch.object(inventory.aiohttp,'ClientSession',return_value=client):
|
||
|
|
item = asyncio.run(inventory.fetch_inventory_record(12))
|
||
|
|
assert item['name'] == 'Mac'
|
||
|
|
assert item['ram_gb'] == 24
|
||
|
|
assert session.get.call_args.kwargs['params']['page'] == 2
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_inventory_and_list_failure_do_not_pair_wrong_machine():
|
||
|
|
for status, expected in [(200,404),(503,502)]:
|
||
|
|
client, session = response_session(404,{})
|
||
|
|
list_client, _ = response_session(status,{'data':[{'id':99}], 'pagination':{'total_pages':1}})
|
||
|
|
session.get.side_effect = [session.get.return_value,list_client.__aenter__.return_value.get.return_value]
|
||
|
|
with patch.object(inventory,'_apigw_headers',return_value={}), patch.object(inventory.aiohttp,'ClientSession',return_value=client):
|
||
|
|
with pytest.raises(HTTPException) as error:
|
||
|
|
asyncio.run(inventory.fetch_inventory_record(12))
|
||
|
|
assert error.value.status_code == expected
|