feat: scoped responsibility selectors, home card layouts and network topology

This commit is contained in:
Codex
2026-10-05 19:07:19 +08:00
parent 675d31c01f
commit cf895110ee
16 changed files with 356 additions and 32 deletions
+88
View File
@@ -0,0 +1,88 @@
"""Responsibility references and personal household layout regressions."""
import unittest
from test_app import app
import test_households as fixture
class InventoryTests(unittest.TestCase):
setUp = fixture.HouseholdTests.setUp
stop_server = fixture.HouseholdTests.stop_server
request = fixture.HouseholdTests.request
def test_candidates_are_scoped_minimal_and_support_current_superadmin(self):
options = self.request('/api/state')[1]['responsibilityOptions']
refs = {p['ref'] for p in options}
self.assertIn('user:'+self.members[self.aid]['id'], refs)
self.assertNotIn('user:'+self.members[self.bid]['id'], refs)
self.assertTrue(all(set(p)=={'ref','name','label','kind'} for p in options))
root_options = app.INVENTORY.candidates(self.roots[self.bid])
self.assertIn('user:'+self.root['id'], {p['ref'] for p in root_options})
self.assertNotIn('responsibilityOptions', self.request('/api/state', self.members[self.aid])[1])
def test_reference_names_are_authoritative_and_cannot_cross_households(self):
member = self.members[self.aid]
data = dict(self.assets[self.aid],ownerRef='user:'+member['id'],ownerName='spoofed')
code, saved = self.request('/api/assets',data=data)
self.assertEqual(code,200);self.assertEqual(saved['ownerName'],member['name'])
data['managerRef']='user:'+self.members[self.bid]['id']
self.assertEqual(self.request('/api/assets',data=data)[0],400)
app.ACCOUNTS.save(dict(app.get_object('users',member['id']),name='更新名字'),self.roots[self.aid])
shown=self.request('/api/state')[1]['assets'][0]
self.assertEqual(shown['ownerName'],'更新名字');self.assertEqual(shown['ownerRef'],saved['ownerRef'])
self.assertEqual(self.request('/api/assets',data=dict(saved,ownerRef='',ownerName='共有单位'))[1]['ownerName'],'共有单位')
def test_archived_or_disabled_links_are_preserved_but_not_newly_assigned(self):
member=self.members[self.aid]
saved=app.INVENTORY.save(dict(name='测试灯',deviceType='灯',managerRef='user:'+member['id']),self.owner)
app.ACCOUNTS.save(dict(app.get_object('users',member['id']),disabled=True),self.roots[self.aid])
self.assertEqual(app.INVENTORY.save(saved,self.owner)['managerRef'],saved['managerRef'])
with self.assertRaises(app.Problem):app.INVENTORY.save(dict(name='新灯',deviceType='灯',managerRef=saved['managerRef']),self.owner)
def test_people_and_all_device_types_share_references_without_granting_access(self):
person=dict(id='e'*16,name='家庭成员',familyId=self.aid,archived=False)
app.save_object('people',person)
ref='person:'+person['id'];self.assertIn(ref,{p['ref'] for p in app.INVENTORY.candidates(self.owner)})
before=app.objects('users')
rows=[app.save_asset(dict(self.assets[self.aid],managerRef=ref),self.owner),
app.save_recorder(dict(name='录像机',siteId=self.sites[self.aid]['id'],host='192.168.9.50',managerRef=ref),self.owner),
app.INVENTORY.save(dict(name='灯',deviceType='灯',managerRef=ref),self.owner)]
self.assertTrue(all(r['managerName']=='家庭成员' for r in rows));self.assertEqual(before,app.objects('users'))
def test_layout_persists_per_account_and_rejects_conflicts_and_cross_scope(self):
data=dict(revision=0,columns=3,rows=2,groupCity=False,order=[self.bid,self.aid])
code,saved=self.request('/api/households/layout',self.root,data)
self.assertEqual(code,200);self.assertEqual(saved['order'],data['order'])
self.assertEqual(self.request('/api/state',self.root)[1]['householdLayout']['columns'],3)
self.assertEqual(app.HOUSEHOLDS.layout(self.owner)['columns'],2)
self.assertEqual(self.request('/api/households/layout',self.root,data)[0],409)
self.assertEqual(self.request('/api/households/layout',self.owner,data)[0],400)
for bad in [dict(data,revision=1,columns=0),dict(data,revision=1,rows=True),dict(data,revision=1,order=[self.aid,self.aid]),dict(data,revision=1,userId=self.owner['id'])]:
self.assertEqual(self.request('/api/households/layout',self.root,bad)[0],400)
member=self.members[self.aid]
self.assertEqual(self.request('/api/households/layout',member,dict(data,order=[self.aid]))[0],200)
def test_topology_references_and_network_fields_validate_without_opening_access(self):
gateway=app.INVENTORY.save(dict(name='网关',deviceType='路由器',managementHost='192.168.8.1',tailscaleHost='gateway.example.ts.net',networkCidr='192.168.8.0/24',networkAccess='tailscale'),self.owner)
camera=app.save_asset(dict(self.assets[self.aid],uplinkId=gateway['id'],connectionType='wired'),self.owner)
with self.assertRaises(app.Problem):app.INVENTORY.save(dict(gateway,uplinkId=camera['id']),self.owner)
with self.assertRaises(app.Problem):app.INVENTORY.save(dict(gateway,uplinkId=gateway['id']),self.owner)
with self.assertRaises(app.Problem):app.save_asset(dict(camera,uplinkId=self.assets[self.bid]['id']),self.owner)
for bad in [dict(networkCidr='192.168.8.160/24'),dict(managementHost='http://user:secret@example.com'),dict(networkAccess='all'),dict(connectionType='anything')]:
with self.assertRaises(app.Problem):app.INVENTORY.save(dict(gateway,**bad),self.owner)
self.assertEqual(self.request('/api/state',self.other)[1]['devices'],[])
self.assertEqual(app.INVENTORY.save(dict(gateway,networkAccess='public',managementHost='example.org'),self.owner)['managementHost'],'example.org')
def test_global_topology_summaries_count_only_authorized_devices(self):
app.INVENTORY.save(dict(name='未授权区域的设备',deviceType='路由器',networkAccess='tailscale'),self.owner)
own=self.request('/api/state',self.members[self.aid])[1]['households']
self.assertEqual(len(own),1)
self.assertEqual(own[0]['deviceCount'],1)
self.assertNotIn('tailscale',own[0]['networkAccesses'])
managed=self.request('/api/state',self.owner)[1]['households'][0]
self.assertEqual(managed['deviceCount'],2)
self.assertIn('tailscale',managed['networkAccesses'])
overall=self.request('/api/state',self.root)[1]['households']
self.assertEqual({h['id']:h['deviceCount'] for h in overall},{self.aid:2,self.bid:1})
if __name__=='__main__':unittest.main()