ui rehaul
This commit is contained in:
+109
-7
@@ -18,10 +18,14 @@ const data={initialized:true,fetchedAt:new Date().toISOString(),users:[
|
||||
...Array.from({length:77},(_,i)=>({sid:'user-'+i,sam:'mitarbeiter.'+String(i+1).padStart(2,'0'),name:'Mitarbeiter '+(i+1),available:true}))
|
||||
],folders:[
|
||||
{id:'invoices',name:'Rechnungen',active:true,permissions:[{kind:'user',principalId:'alice',level:2},{kind:'user',principalId:'bob',level:1},{kind:'user',principalId:'carol',level:0}]},
|
||||
{id:'projects',name:'Projektunterlagen',active:true,permissions:[{kind:'user',principalId:'carol',level:3}]},
|
||||
{id:'projects',name:'Projektunterlagen und technische Dokumentation internationaler Niederlassungen für das laufende Geschäftsjahr',active:true,permissions:[{kind:'user',principalId:'carol',level:3}]},
|
||||
{id:'archive',name:'Abschluss 2024',active:false,permissions:[]}
|
||||
]};
|
||||
let nextId=0, failSave=false; const requests=[],errors=[];
|
||||
const trashItems=[
|
||||
{id:'data/report',share:'Data',path:'Finance/Budget.xlsx',size:14336,deletedAt:'2026-10-03T09:20:00Z',deletedBy:'EXAMPLE\\alice',expiresAt:'2026-10-10T09:20:00Z'},
|
||||
{id:'private/note',share:'Private',path:'Notizen.txt',size:1536,deletedAt:'2026-10-03T08:45:00Z',deletedBy:'EXAMPLE\\bob',expiresAt:'2026-10-10T08:45:00Z'}
|
||||
];
|
||||
let nextId=0, failSave=false; const requests=[],errors=[],restores=[];
|
||||
(async()=>{
|
||||
const browser=await chromium.launch({headless:true});
|
||||
const page=await browser.newPage({viewport:{width:1500,height:1050}});
|
||||
@@ -37,6 +41,11 @@ let nextId=0, failSave=false; const requests=[],errors=[];
|
||||
if(change.action==='create-folder')data.folders.push({id:'folder-'+(++nextId),name:change.name,active:true,permissions:[]});
|
||||
if(change.action==='archive-folder'||change.action==='restore-folder')data.folders.find(f=>f.id===change.id).active=change.action==='restore-folder';
|
||||
}body=data;
|
||||
} else if(url.pathname==='/api/trash')body={items:trashItems,matched:trashItems.length,retentionDays:7,truncated:false};
|
||||
else if(url.pathname==='/api/trash/restore'){
|
||||
const change=route.request().postDataJSON();restores.push(change);
|
||||
const item=trashItems.find(item=>item.id===change.id);
|
||||
trashItems.splice(trashItems.indexOf(item),1);body={share:item.share,path:item.path};
|
||||
} else if(url.pathname==='/assets/app.js'){body=fs.readFileSync(root+'/app.js');type='text/javascript';}
|
||||
else if(url.pathname==='/assets/styles.css'){body=fs.readFileSync(root+'/styles.css');type='text/css';}
|
||||
else {body=fs.readFileSync(root+'/index.html');type='text/html';}
|
||||
@@ -44,6 +53,16 @@ let nextId=0, failSave=false; const requests=[],errors=[];
|
||||
});
|
||||
const settled=()=>page.waitForFunction(()=>document.querySelector('#access-message')?.textContent==='Gespeichert. Die Änderungen sind wirksam.'&&!document.querySelector('#access-create').disabled);
|
||||
const shot=async name=>page.screenshot({path:out+'/'+name+'.png',fullPage:true});
|
||||
const checkShareNameEllipsis=async()=>{
|
||||
const longName=page.locator('[data-select=projects] strong');
|
||||
assert.equal(await longName.getAttribute('title'),data.folders.find(f=>f.id==='projects').name);
|
||||
assert.equal(await longName.evaluate(element=>element.scrollWidth>element.clientWidth),true,'Long share name is not clipped');
|
||||
const heights=await page.evaluate(()=>({
|
||||
long:document.querySelector('[data-select=projects]').getBoundingClientRect().height,
|
||||
short:document.querySelector('[data-select=invoices]').getBoundingClientRect().height
|
||||
}));
|
||||
assert.ok(Math.abs(heights.long-heights.short)<=1,'Long share name wraps and increases row height');
|
||||
};
|
||||
const checkSharedStyle=async()=>{
|
||||
const differences=await page.evaluate(()=>{
|
||||
const mismatches=[];
|
||||
@@ -68,12 +87,49 @@ let nextId=0, failSave=false; const requests=[],errors=[];
|
||||
});
|
||||
assert.deepEqual(differences,[],'Access controls differ from the established admin styles');
|
||||
};
|
||||
await page.goto('http://adfs.test/access');await page.locator('[data-user-rule]').first().waitFor();
|
||||
await page.goto('http://adfs.test/shares');await page.locator('[data-user-rule]').first().waitFor();
|
||||
assert.equal(new URL(page.url()).pathname,'/access');
|
||||
assert.equal(await page.locator('nav a[href="/shares"]').count(),0);
|
||||
assert.equal(await page.locator('[data-tab=groups], [data-group-rule]').count(),0);
|
||||
assert.doesNotMatch(await page.locator('#content').innerText(),/Gruppe/);
|
||||
assert.equal(await page.locator('#access-users .access-matrix-row').count(),80);
|
||||
await checkSharedStyle();
|
||||
await checkShareNameEllipsis();
|
||||
await shot('01-desktop-benutzerrechte');
|
||||
// Filters remain global to the access page while each folder loads its own rights.
|
||||
await page.locator('#access-search').fill('r');
|
||||
await page.selectOption('#folder-status','all');
|
||||
for(const filter of ['granted','hidden','all']){
|
||||
await page.locator('#access-user-search').fill('alice');
|
||||
await page.selectOption('#access-user-filter',filter);
|
||||
await page.locator('[data-select=projects]').click();
|
||||
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
||||
assert.equal(await page.locator('#access-user-filter').inputValue(),filter);
|
||||
assert.equal(await page.locator('#access-search').inputValue(),'r');
|
||||
assert.equal(await page.locator('#folder-status').inputValue(),'all');
|
||||
assert.equal(await page.locator('#access-users .access-matrix-row').count(),filter==='granted'?0:1);
|
||||
if(filter==='granted'){
|
||||
assert.match(await page.locator('#access-users').innerText(),/Keine Benutzer/);
|
||||
await shot('08-folder-switch-retains-empty-filter');
|
||||
}
|
||||
await page.locator('[data-select=invoices]').click();
|
||||
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
||||
assert.equal(await page.locator('#access-user-filter').inputValue(),filter);
|
||||
assert.equal(await page.locator('#access-users .access-matrix-row').count(),filter==='hidden'?0:1);
|
||||
}
|
||||
await page.locator('#access-search').fill('');
|
||||
await page.selectOption('#access-user-filter','granted');
|
||||
await page.selectOption('#folder-status','archived');
|
||||
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
||||
assert.equal(await page.locator('#access-user-filter').inputValue(),'granted');
|
||||
assert.equal(await page.locator('#access-users .access-matrix-row').count(),0);
|
||||
await page.selectOption('#folder-status','active');
|
||||
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
||||
assert.equal(await page.locator('#access-user-filter').inputValue(),'granted');
|
||||
assert.equal(await page.locator('#access-users .access-matrix-row').count(),1);
|
||||
await shot('09-folder-switch-retains-matching-filter');
|
||||
await page.locator('#access-user-search').fill('');
|
||||
await page.selectOption('#access-user-filter','all');
|
||||
assert.equal(await page.locator('#access-save').isDisabled(),true);
|
||||
await page.locator('#access-user-search').fill('alice');
|
||||
await page.locator('[data-user-rule=alice][data-level="3"]').click();
|
||||
@@ -115,25 +171,43 @@ let nextId=0, failSave=false; const requests=[],errors=[];
|
||||
await page.selectOption('#access-user-filter','granted');
|
||||
assert.equal(await page.locator('[data-user-rule=bob][data-level="2"]').getAttribute('aria-pressed'),'true');
|
||||
assert.equal(await page.locator('#access-save').isDisabled(),true);
|
||||
await page.locator('[data-select=projects]').click();
|
||||
await page.selectOption('#access-user-filter','hidden');
|
||||
await page.locator('#access-user-search').fill('alice');
|
||||
await page.locator('[data-select=projects]').click();
|
||||
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
||||
assert.equal(await page.locator('#access-user-filter').inputValue(),'hidden');
|
||||
assert.equal(await page.locator('[data-user-rule=alice][data-level="0"]').getAttribute('aria-pressed'),'true');
|
||||
await page.locator('[data-user-rule=alice][data-level="1"]').click();
|
||||
await page.locator('#access-save').click();await settled();
|
||||
await page.locator('[data-select=invoices]').click();
|
||||
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
||||
assert.equal(await page.locator('#access-user-filter').inputValue(),'hidden');
|
||||
assert.equal(await page.locator('#access-users .access-matrix-row').count(),0);
|
||||
await page.selectOption('#access-user-filter','granted');
|
||||
assert.equal(await page.locator('[data-user-rule=alice][data-level="3"]').getAttribute('aria-pressed'),'true');
|
||||
await page.locator('#access-create').click();
|
||||
await page.locator('.access-dialog input').fill('Neue Ablage');
|
||||
await checkSharedStyle();await shot('04-desktop-ordner-anlegen');
|
||||
await page.locator('.access-dialog button[type=submit]').click();await settled();
|
||||
assert.match(await page.locator('.access-detail-head h2').innerText(),/Neue Ablage/);
|
||||
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
||||
assert.equal(await page.locator('#access-user-filter').inputValue(),'granted');
|
||||
assert.equal(await page.locator('#access-users .access-matrix-row').count(),0);
|
||||
await page.locator('#access-user-search').fill('');
|
||||
await page.selectOption('#access-user-filter','all');
|
||||
assert.equal(await page.locator('#access-users button[aria-pressed=true][data-level="0"]').count(),80);
|
||||
await page.locator('#access-archive').click();
|
||||
await page.locator('.access-dialog button[type=submit]').click();await settled();
|
||||
assert.equal(await page.locator('#folder-status').inputValue(),'archived');
|
||||
assert.equal(await page.locator('[data-user-rule]').first().isDisabled(),true);
|
||||
await page.locator('#access-archive').click();
|
||||
await page.locator('.access-dialog button[type=submit]').click();await settled();
|
||||
const folderRestore=page.getByRole('button',{name:'Wiederherstellen',exact:true});
|
||||
assert.equal(await folderRestore.getAttribute('title'),'Wiederherstellen');
|
||||
assert.equal(await folderRestore.innerText(),'');
|
||||
await folderRestore.locator('svg').click();
|
||||
const confirmRestore=page.locator('.access-dialog').getByRole('button',{name:'Wiederherstellen',exact:true});
|
||||
assert.equal(await confirmRestore.innerText(),'');
|
||||
await shot('10-folder-restore-icon');
|
||||
await confirmRestore.locator('svg').click();await settled();
|
||||
assert.equal(await page.locator('#folder-status').inputValue(),'active');
|
||||
await page.locator('[data-select=invoices]').click();
|
||||
await page.selectOption('#access-user-filter','granted');
|
||||
@@ -146,6 +220,7 @@ let nextId=0, failSave=false; const requests=[],errors=[];
|
||||
return footer.getBoundingClientRect().top >= section.getBoundingClientRect().bottom-1;
|
||||
}),true,'Save controls cover form content at '+width);
|
||||
await checkSharedStyle();
|
||||
await checkShareNameEllipsis();
|
||||
await shot('05-benutzerrechte-'+width);
|
||||
}
|
||||
await page.selectOption('#access-user-filter','all');
|
||||
@@ -184,8 +259,35 @@ let nextId=0, failSave=false; const requests=[],errors=[];
|
||||
await page.locator('#access-create').waitFor();
|
||||
assert.match(await page.locator('#access-detail').innerText(),/Ersten Ordner anlegen/);
|
||||
await shot('07-desktop-leere-installation');
|
||||
// Icon controls keep accessible names and activate their original actions.
|
||||
await page.goto('http://adfs.test/trash');
|
||||
await page.locator('#trash-rows .row-actions').first().waitFor();
|
||||
for(const [role,name] of [['link','Herunterladen'],['button','Wiederherstellen']]){
|
||||
const control=page.getByRole(role,{name,exact:true}).first();
|
||||
assert.equal(await control.getAttribute('title'),name);
|
||||
assert.equal(await control.innerText(),'');
|
||||
assert.equal(await control.locator('svg').getAttribute('aria-hidden'),'true');
|
||||
}
|
||||
await shot('11-trash-action-icons-desktop');
|
||||
const downloadStarted=page.waitForEvent('download');
|
||||
await page.getByRole('link',{name:'Herunterladen',exact:true}).first().locator('svg').click();
|
||||
const download=await downloadStarted;
|
||||
assert.equal(download.url(),'http://adfs.test/api/trash/download?id=data%2Freport');
|
||||
// Downloads leave page interception; verify the destination and cancel the mocked transfer.
|
||||
await download.cancel();
|
||||
await page.getByRole('button',{name:'Wiederherstellen',exact:true}).first().locator('svg').click();
|
||||
assert.equal(restores.length,0,'Cancelling restoration should not submit');
|
||||
page.removeAllListeners('dialog');
|
||||
page.on('dialog',dialog=>dialog.accept());
|
||||
await page.getByRole('button',{name:'Wiederherstellen',exact:true}).first().locator('svg').click();
|
||||
await page.waitForFunction(()=>document.querySelectorAll('#trash-rows .row-actions').length===1);
|
||||
assert.deepEqual(restores,[{id:'data/report'}]);
|
||||
await page.setViewportSize({width:390,height:844});
|
||||
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false,'Trash actions cause page overflow');
|
||||
await page.locator('.table-wrap').evaluate(table=>{table.scrollLeft=table.scrollWidth;});
|
||||
await shot('12-trash-action-icons-mobile');
|
||||
assert.deepEqual(errors,[]);
|
||||
console.log('PASS: individual permissions, no groups, user search/filter persistence, unsaved-change protection, failure/retry, independent folders, create/archive/restore, desktop/mobile layout, shared admin styling.');
|
||||
console.log('PASS: individual permissions, no groups, user search/filter persistence, unsaved-change protection, failure/retry, independent folders, create/archive/restore, desktop/mobile layout, shared admin styling, retired shares route, restore/download icons and actions.');
|
||||
console.log('Screenshots: '+out);
|
||||
await browser.close();
|
||||
})().catch(error=>{console.error(error);process.exit(1)});
|
||||
|
||||
@@ -39,6 +39,90 @@ class ManagedAccessTests(unittest.TestCase):
|
||||
data = self.change({'action': 'create-folder', 'name': name})
|
||||
return next(row['id'] for row in data['folders'] if row['name'] == name)
|
||||
|
||||
def test_excluded_users_hidden_in_snapshots_reports_and_effective_permissions(self):
|
||||
folder = self.folder()
|
||||
banned = {ALICE: {'sam': 'MSOL_sync', 'name': 'Sync'},
|
||||
BOB: {'sam': 'KrBtGt', 'name': 'Ticket Service'}}
|
||||
access.cache_users(self.conn, banned)
|
||||
self.conn.executemany('INSERT INTO folder_permissions VALUES(?,?,?,?)',
|
||||
[(folder, 'user', ALICE, 3), (folder, 'user', BOB, 2)])
|
||||
self.conn.commit()
|
||||
self.assertEqual(access.snapshot(self.conn, banned)['users'], [])
|
||||
self.assertEqual(access.snapshot(self.conn)['folders'][0]['permissions'], [])
|
||||
self.assertEqual(access.effective_levels(self.conn, folder), {})
|
||||
report = access.report_folders(self.conn)['groups'][0]
|
||||
self.assertEqual(report['members'], [])
|
||||
self.assertEqual(report['userCount'], 0)
|
||||
|
||||
def test_excluded_users_cannot_receive_new_or_old_positive_grants(self):
|
||||
folder = self.folder()
|
||||
banned = {ALICE: {'sam': 'mSoL_sync', 'name': 'Sync'},
|
||||
BOB: {'sam': 'KRBTGT', 'name': 'Ticket Service'}}
|
||||
access.cache_users(self.conn, banned)
|
||||
self.conn.execute('INSERT INTO folder_permissions VALUES(?,?,?,?)', (folder, 'user', ALICE, 3))
|
||||
self.conn.commit()
|
||||
with mock.patch.object(access, 'sync_permissions'), mock.patch.object(directory, 'run_command', return_value=mock.Mock(returncode=0)):
|
||||
# Both current-directory identities and the unavailable-user fallback
|
||||
# must reject positive grants; removing a legacy grant remains possible.
|
||||
for users, sid in [(banned, ALICE), (banned, BOB), ({}, ALICE)]:
|
||||
for level in (1, 2, 3):
|
||||
with self.subTest(users=users, sid=sid, level=level):
|
||||
with self.assertRaisesRegex(ValueError, 'Systemkonto'):
|
||||
access.apply_change(self.conn, {'action': 'set-permissions', 'id': folder,
|
||||
'permissions': [{'kind': 'user', 'principalId': sid, 'level': level}]}, 'admin', users)
|
||||
access.apply_change(self.conn, {'action': 'set-permissions', 'id': folder,
|
||||
'permissions': [{'kind': 'user', 'principalId': ALICE, 'level': 0}]}, 'admin', {})
|
||||
self.assertEqual(access.effective_levels(self.conn, folder), {})
|
||||
|
||||
def test_sync_revokes_old_grants_recursively_preserving_data_and_retries_disconnect(self):
|
||||
folder = self.folder()
|
||||
nested = os.path.join(directory.GROUP_ROOT, 'Finance', 'nested')
|
||||
os.mkdir(nested)
|
||||
path = os.path.join(nested, 'retained.bin')
|
||||
with open(path, 'wb') as handle:
|
||||
handle.write(b'production data must survive')
|
||||
inode = os.stat(path).st_ino
|
||||
access.cache_users(self.conn, {ALICE: {'sam': 'MSOL_sync', 'name': 'Sync'}})
|
||||
self.conn.executemany('INSERT INTO folder_permissions VALUES(?,?,?,?)',
|
||||
[(folder, 'user', ALICE, 3), (folder, 'user', BOB, 2)])
|
||||
self.conn.commit()
|
||||
with mock.patch.dict(os.environ, {'DOMAIN_ADMINS_SID': ADMIN}), mock.patch.object(access, 'pack_descriptor', side_effect=lambda s: s), mock.patch.object(access, 'set_acl') as set_acl:
|
||||
access.sync_permissions(self.conn)
|
||||
self.conn.commit()
|
||||
self.assertEqual(access.effective_levels(self.conn, folder), {BOB: 2})
|
||||
self.assertIsNone(self.conn.execute('SELECT 1 FROM folder_permissions WHERE principalId=?', (ALICE,)).fetchone())
|
||||
self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM access_changes WHERE action='revoke-excluded-users'").fetchone()[0], 1)
|
||||
self.assertTrue(any(call.args[0] == path for call in set_acl.call_args_list))
|
||||
self.assertTrue(all(ALICE not in call.args[1] for call in set_acl.call_args_list))
|
||||
with mock.patch.object(directory, 'run_command', side_effect=[mock.Mock(returncode=1), mock.Mock(returncode=0)]) as run:
|
||||
with self.assertRaises(RuntimeError):
|
||||
access.close_data_connections(self.conn)
|
||||
self.assertIsNotNone(self.conn.execute("SELECT 1 FROM access_settings WHERE key='pendingExcludedRevocation'").fetchone())
|
||||
access.close_data_connections(self.conn)
|
||||
access.close_data_connections(self.conn)
|
||||
self.assertEqual(run.call_count, 2)
|
||||
self.assertIsNone(self.conn.execute("SELECT 1 FROM access_settings WHERE key='pendingExcludedRevocation'").fetchone())
|
||||
with open(path, 'rb') as handle:
|
||||
self.assertEqual(handle.read(), b'production data must survive')
|
||||
self.assertEqual(os.stat(path).st_ino, inode)
|
||||
|
||||
def test_migration_resolves_excluded_nested_and_primary_members_without_grants(self):
|
||||
folder = self.folder()
|
||||
self.conn.execute("UPDATE shares SET samAccountName='FS_Finance' WHERE objectGUID=?", (folder,))
|
||||
self.conn.commit()
|
||||
ticket = 'S-1-5-21-1-2-3-502'
|
||||
users = {ALICE: {**USERS[ALICE], 'dn': 'alice', 'primaryRid': 513},
|
||||
BOB: {'sam': 'MSOL_sync', 'name': 'Sync', 'dn': 'sync', 'primaryRid': 1200},
|
||||
ticket: {'sam': 'krbtgt', 'name': 'Ticket', 'dn': 'ticket', 'primaryRid': 513}}
|
||||
groups = {'finance': {'dn': 'finance', 'sam': 'FS_Finance', 'sid': 'S-1-5-21-1-2-3-1200', 'members': ['sync', 'nested']},
|
||||
'nested': {'dn': 'nested', 'sam': 'Nested', 'sid': 'S-1-5-21-1-2-3-513', 'members': ['finance', 'ticket']}}
|
||||
with mock.patch.object(access, 'read_directory', return_value=(users, groups)):
|
||||
access.migrate(self.conn)
|
||||
self.assertTrue(access.initialized(self.conn))
|
||||
self.assertEqual(access.effective_levels(self.conn, folder), {ALICE: 3})
|
||||
self.assertEqual([row['principalId'] for row in self.conn.execute('SELECT principalId FROM folder_permissions')], [ALICE])
|
||||
self.assertEqual([row['sam'] for row in access.snapshot(self.conn)['users']], ['alice'])
|
||||
|
||||
def test_individual_user_assignments_are_independent_including_zero(self):
|
||||
folder = self.folder()
|
||||
other = self.folder('Projects')
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import unittest
|
||||
|
||||
from app.account_policy import is_excluded_user
|
||||
|
||||
|
||||
class AccountPolicyTests(unittest.TestCase):
|
||||
def test_excludes_exact_krbtgt_and_msol_prefix_case_insensitively(self):
|
||||
for name in ("krbtgt", "KRBTGT", "MSOL_", "mSoL_123", "DOMAIN\\MSOL_sync",
|
||||
"krbtgt@example.test", " DOMAIN\\KrBtGt "):
|
||||
with self.subTest(name=name):
|
||||
self.assertTrue(is_excluded_user(name))
|
||||
|
||||
def test_keeps_human_and_other_service_accounts(self):
|
||||
for name in ("alice", "Administrator", "report_svc", "MsolSupport", "krbtgt_support", ""):
|
||||
with self.subTest(name=name):
|
||||
self.assertFalse(is_excluded_user(name))
|
||||
+46
-5
@@ -646,6 +646,20 @@ class AuditQueryTests(unittest.TestCase):
|
||||
"source": "log.pc01",
|
||||
}
|
||||
|
||||
def test_excluded_accounts_removed_from_user_choices_but_audit_records_retained(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
today = dt.datetime.now(dt.timezone.utc).date()
|
||||
database = os.path.join(tmpdir, 'state.db')
|
||||
store = audit_store.AuditStore(database)
|
||||
names = ['alice', 'DOMAIN\\MSOL_sync', 'KrBtGt@example.test']
|
||||
store.append_batch([self.make_event(f'{today}T12:00:0{index}+00:00', name)
|
||||
for index, name in enumerate(names)], {}, set())
|
||||
store.close()
|
||||
with mock.patch.object(web_ui, 'STATE_DB', database):
|
||||
result = web_ui.query_audit({'from': [str(today)], 'to': [str(today)], 'facets': ['1']})
|
||||
self.assertEqual(result['facets']['users'], ['alice'])
|
||||
self.assertEqual({row['user'] for row in result['events']}, set(names))
|
||||
|
||||
def test_queries_indexed_events_with_filters_and_keyset_cursor(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
today = dt.datetime.now(dt.timezone.utc).date()
|
||||
@@ -1214,11 +1228,9 @@ class WebPresentationTests(unittest.TestCase):
|
||||
self.assertNotIn(">Metadaten<", script)
|
||||
self.assertNotIn(">Sitzung<", script)
|
||||
self.assertNotIn('name="operation"', script)
|
||||
self.assertIn('class="shares-view"', script)
|
||||
self.assertIn('class="split shares-split"', script)
|
||||
self.assertIn(".shares-view { display: flex; height: calc(100vh - 4.5rem);", css)
|
||||
self.assertIn(".shares-split .list", css)
|
||||
self.assertIn(".shares-split .tree", css)
|
||||
self.assertNotIn('href="/shares"', html)
|
||||
self.assertNotIn('Dateifreigaben', html)
|
||||
self.assertNotIn('renderShares', script)
|
||||
|
||||
def test_admin_actions_are_german_and_backup_automatic_setting_is_env_only(self):
|
||||
script = self.asset("app.js")
|
||||
@@ -1361,6 +1373,35 @@ class TlsSummaryTests(unittest.TestCase):
|
||||
|
||||
|
||||
class UsageScannerTests(unittest.TestCase):
|
||||
def test_excluded_user_storage_rows_hidden_without_losing_files_or_totals(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
roots = {name: os.path.join(tmpdir, name) for name in ('GROUP_ROOT', 'PRIVATE_ROOT', 'FSLOGIX_ROOT')}
|
||||
for path in roots.values():
|
||||
os.mkdir(path)
|
||||
for name in ('alice', 'MSOL_sync', 'KrBtGt'):
|
||||
path = os.path.join(roots['PRIVATE_ROOT'], name)
|
||||
os.mkdir(path)
|
||||
with open(os.path.join(path, 'retained.txt'), 'wb') as handle:
|
||||
handle.write(b'123')
|
||||
profile = os.path.join(roots['FSLOGIX_ROOT'], 'sync-profile')
|
||||
os.mkdir(profile)
|
||||
with open(os.path.join(profile, 'retained.vhdx'), 'wb') as handle:
|
||||
handle.write(b'12345')
|
||||
with mock.patch.dict(os.environ, roots), mock.patch.object(web_ui, 'STATE_DB', os.path.join(tmpdir, 'state.db')), mock.patch.object(web_ui, 'fslogix_username', return_value='MSOL_sync'):
|
||||
scanner = web_ui.UsageScanner()
|
||||
value = scanner.scan()
|
||||
self.assertEqual([row['name'] for row in value['users']], ['alice'])
|
||||
self.assertEqual(value['totals']['privateBytes'], 9)
|
||||
self.assertEqual(value['totals']['fslogixBytes'], 5)
|
||||
# Old persisted cache entries are filtered before reaching any page/report.
|
||||
scanner.data['users'].append({'name': 'DOMAIN\\krbtgt', 'totalBytes': 3})
|
||||
self.assertEqual([row['name'] for row in scanner.snapshot()['users']], ['alice'])
|
||||
self.assertEqual(scanner.snapshot()['totals'], value['totals'])
|
||||
for name in ('MSOL_sync', 'KrBtGt'):
|
||||
with open(os.path.join(roots['PRIVATE_ROOT'], name, 'retained.txt'), 'rb') as handle:
|
||||
self.assertEqual(handle.read(), b'123')
|
||||
self.assertTrue(os.path.isfile(os.path.join(profile, 'retained.vhdx')))
|
||||
|
||||
def test_aggregates_private_and_fslogix_by_user(self):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
group_root = os.path.join(tmpdir, "data")
|
||||
|
||||
Reference in New Issue
Block a user