simple-module-sharing 0.0.38__tar.gz → 0.0.39__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/PKG-INFO +5 -5
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/package.json +1 -1
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/pyproject.toml +5 -5
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/locales/de.json +2 -1
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/locales/en.json +2 -1
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/module.py +8 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/pages/Admin.tsx +8 -0
- simple_module_sharing-0.0.39/sharing/tenant_cleanup.py +16 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/tests/test_sharing_service.py +61 -0
- simple_module_sharing-0.0.39/tests/test_sharing_tenant_cleanup.py +43 -0
- simple_module_sharing-0.0.39/tests-js/sharing_Admin.test.tsx +110 -0
- simple_module_sharing-0.0.39/tests-js/sharing_Admin_errors.test.tsx +61 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/.gitignore +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/LICENSE +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/README.md +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/__init__.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/audience.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/constants.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/contracts/__init__.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/contracts/schemas.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/deps.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/endpoints/__init__.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/endpoints/api.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/endpoints/views.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/models.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/pages/components/ShareTable.tsx +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/pages/types.ts +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/policy.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/present.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/py.typed +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/service.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/services.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/sharing/settings.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/tests/_sharing_support.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/tests/test_sharing_http.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/tests/test_sharing_tenant_audience.py +0 -0
- {simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_sharing
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.39
|
|
4
4
|
Summary: Generic resource shares (owner / shared-with-user) for simple_module resource-scoped access
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -11,10 +11,10 @@ License-Expression: MIT
|
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Keywords: access-control,permissions,sharing,simple-module
|
|
13
13
|
Requires-Python: >=3.12
|
|
14
|
-
Requires-Dist: simple-module-core==0.0.
|
|
15
|
-
Requires-Dist: simple-module-db==0.0.
|
|
16
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
17
|
-
Requires-Dist: simple-module-inertia==0.0.
|
|
14
|
+
Requires-Dist: simple-module-core==0.0.39
|
|
15
|
+
Requires-Dist: simple-module-db==0.0.39
|
|
16
|
+
Requires-Dist: simple-module-hosting==0.0.39
|
|
17
|
+
Requires-Dist: simple-module-inertia==0.0.39
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
20
20
|
# simple_module_sharing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_sharing"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.39"
|
|
4
4
|
description = "Generic resource shares (owner / shared-with-user) for simple_module resource-scoped access"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -9,10 +9,10 @@ requires-python = ">=3.12"
|
|
|
9
9
|
authors = [{ name = "Anto Subash", email = "antosubash@live.com" }]
|
|
10
10
|
keywords = ["simple-module", "sharing", "permissions", "access-control"]
|
|
11
11
|
dependencies = [
|
|
12
|
-
"simple_module_core==0.0.
|
|
13
|
-
"simple_module_db==0.0.
|
|
14
|
-
"simple_module_hosting==0.0.
|
|
15
|
-
"simple_module_inertia==0.0.
|
|
12
|
+
"simple_module_core==0.0.39",
|
|
13
|
+
"simple_module_db==0.0.39",
|
|
14
|
+
"simple_module_hosting==0.0.39",
|
|
15
|
+
"simple_module_inertia==0.0.39",
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
[project.entry-points.simple_module]
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"title": "Diese Freigabe widerrufen?",
|
|
29
29
|
"description": "Die Person verliert sofort den Zugriff auf die Ressource.",
|
|
30
30
|
"confirm": "Widerrufen",
|
|
31
|
-
"cancel": "Abbrechen"
|
|
31
|
+
"cancel": "Abbrechen",
|
|
32
|
+
"failed": "Die Freigabe konnte nicht widerrufen werden"
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -17,6 +17,7 @@ from sharing import constants as c
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
18
18
|
from fastapi import APIRouter, FastAPI
|
|
19
19
|
from simple_module_core.audit_links import AuditLinkRegistry
|
|
20
|
+
from simple_module_core.events import EventBus
|
|
20
21
|
from simple_module_core.permissions import PermissionRegistry
|
|
21
22
|
|
|
22
23
|
|
|
@@ -27,6 +28,13 @@ class SharingModule(ModuleBase):
|
|
|
27
28
|
view_prefix=c.VIEW_PREFIX,
|
|
28
29
|
)
|
|
29
30
|
|
|
31
|
+
def register_event_handlers(self, bus: EventBus, app: FastAPI | None = None) -> None:
|
|
32
|
+
from simple_module_core.tenant_events import TenantDeleted
|
|
33
|
+
|
|
34
|
+
from sharing.tenant_cleanup import purge_tenant_data
|
|
35
|
+
|
|
36
|
+
bus.subscribe_durable(TenantDeleted, purge_tenant_data)
|
|
37
|
+
|
|
30
38
|
def register_settings(self, app: FastAPI) -> None:
|
|
31
39
|
from sharing.services import SharingServices
|
|
32
40
|
from sharing.settings import SharingSettings
|
|
@@ -7,6 +7,7 @@ import { PaginationBar } from '@simple-module-py/ui/components/PaginationBar';
|
|
|
7
7
|
import { AdminLayout } from '@simple-module-py/ui/layouts/AdminLayout';
|
|
8
8
|
import { Building2, Trash2 } from 'lucide-react';
|
|
9
9
|
import { useState } from 'react';
|
|
10
|
+
import { toast } from 'sonner';
|
|
10
11
|
import { ShareTable } from './components/ShareTable';
|
|
11
12
|
import type { Share } from './types';
|
|
12
13
|
|
|
@@ -37,11 +38,18 @@ function Admin(props: Props) {
|
|
|
37
38
|
function confirmRevoke() {
|
|
38
39
|
if (!pending) return;
|
|
39
40
|
setBusy(true);
|
|
41
|
+
const onFailure = () => {
|
|
42
|
+
toast.error(t(keys.sharing.revoke.failed));
|
|
43
|
+
return false;
|
|
44
|
+
};
|
|
40
45
|
router.post(
|
|
41
46
|
`${BASE_URL}/${pending.id}/revoke`,
|
|
42
47
|
{},
|
|
43
48
|
{
|
|
44
49
|
preserveScroll: true,
|
|
50
|
+
onError: onFailure,
|
|
51
|
+
onHttpException: onFailure,
|
|
52
|
+
onNetworkError: onFailure,
|
|
45
53
|
onFinish: () => {
|
|
46
54
|
setBusy(false);
|
|
47
55
|
setPending(null);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Durable teardown of tenant-owned sharing rows."""
|
|
2
|
+
|
|
3
|
+
from simple_module_core.durable_events import DurableContext
|
|
4
|
+
from simple_module_core.tenant_events import TenantDeleted
|
|
5
|
+
from simple_module_db.tenancy import tenant_context
|
|
6
|
+
from sqlmodel import delete
|
|
7
|
+
|
|
8
|
+
from sharing.models import ResourceShare
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def purge_tenant_data(event: TenantDeleted, ctx: DurableContext) -> None:
|
|
12
|
+
"""Hard-delete tenant rows atomically with outbox acknowledgement."""
|
|
13
|
+
with tenant_context(event.tenant_id):
|
|
14
|
+
await ctx.db.execute(
|
|
15
|
+
delete(ResourceShare).where(ResourceShare.tenant_id == event.tenant_id)
|
|
16
|
+
)
|
|
@@ -14,6 +14,7 @@ from _sharing_support import (
|
|
|
14
14
|
create_note_table,
|
|
15
15
|
register_note_type,
|
|
16
16
|
)
|
|
17
|
+
from sharing.constants import PERM_MANAGE
|
|
17
18
|
from sharing.contracts.schemas import ShareGrant
|
|
18
19
|
from sharing.models import ResourceShare
|
|
19
20
|
from sharing.service import SharingError, SharingService
|
|
@@ -143,3 +144,63 @@ async def test_grantee_may_leave_and_owner_lists(env):
|
|
|
143
144
|
await _refused(service.for_resource(_who(FRIEND), NOTE_TYPE, note_id), 403)
|
|
144
145
|
await service.revoke(_who(FRIEND), share.id)
|
|
145
146
|
assert await _visible(service.db, FRIEND) == []
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@pytest.mark.parametrize("role", [ACTION_VIEW, ACTION_EDIT])
|
|
150
|
+
async def test_grantee_cannot_manage_shares_even_with_edit_role(env, role):
|
|
151
|
+
service, note_id = env
|
|
152
|
+
with tenant_context("t1"):
|
|
153
|
+
await service.grant(_who(OWNER), _grant(note_id, role=role))
|
|
154
|
+
reshare = _grant(note_id).model_copy(update={"subject_id": STRANGER})
|
|
155
|
+
await _refused(service.grant(_who(FRIEND), reshare), 403)
|
|
156
|
+
await _refused(service.for_resource(_who(FRIEND), NOTE_TYPE, note_id), 403)
|
|
157
|
+
assert len(await service.for_resource(_who(OWNER), NOTE_TYPE, note_id)) == 1
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@pytest.mark.parametrize("actor", [OWNER, FRIEND, "admin"])
|
|
161
|
+
async def test_real_share_id_cannot_be_revoked_across_tenants(env, actor):
|
|
162
|
+
service, note_id = env
|
|
163
|
+
who = (
|
|
164
|
+
ResourcePrincipal(user_id=actor, permissions=frozenset({PERM_MANAGE}))
|
|
165
|
+
if actor == "admin"
|
|
166
|
+
else _who(actor)
|
|
167
|
+
)
|
|
168
|
+
with tenant_context("t1"):
|
|
169
|
+
share = await service.grant(_who(OWNER), _grant(note_id))
|
|
170
|
+
with tenant_context("t2"):
|
|
171
|
+
await _refused(service.revoke(who, share.id), 404)
|
|
172
|
+
with tenant_context("t1"):
|
|
173
|
+
assert [s.id for s in await service.for_resource(_who(OWNER), NOTE_TYPE, note_id)] == [
|
|
174
|
+
share.id
|
|
175
|
+
]
|
|
176
|
+
assert await _visible(service.db, FRIEND) == ["n"]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def test_admin_may_revoke_without_resource_ownership(env):
|
|
180
|
+
service, note_id = env
|
|
181
|
+
admin = ResourcePrincipal(user_id=STRANGER, permissions=frozenset({PERM_MANAGE}))
|
|
182
|
+
with tenant_context("t1"):
|
|
183
|
+
share = await service.grant(_who(OWNER), _grant(note_id))
|
|
184
|
+
await service.revoke(admin, share.id)
|
|
185
|
+
assert await service.for_resource(_who(OWNER), NOTE_TYPE, note_id) == []
|
|
186
|
+
assert await _visible(service.db, FRIEND) == []
|
|
187
|
+
await _refused(service.revoke(admin, share.id), 404)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def test_expired_share_can_be_renewed_without_duplicate_grants(env):
|
|
191
|
+
service, note_id = env
|
|
192
|
+
with tenant_context("t1"):
|
|
193
|
+
share = await service.grant(_who(OWNER), _grant(note_id))
|
|
194
|
+
share.expires_at = datetime.now(UTC) - timedelta(days=1)
|
|
195
|
+
await service.db.flush()
|
|
196
|
+
assert await _visible(service.db, FRIEND) == []
|
|
197
|
+
assert [s.id for s in await service.for_resource(_who(OWNER), NOTE_TYPE, note_id)] == [
|
|
198
|
+
share.id
|
|
199
|
+
]
|
|
200
|
+
future = datetime.now(UTC) + timedelta(days=1)
|
|
201
|
+
renewed = await service.grant(
|
|
202
|
+
_who(OWNER), _grant(note_id, expires_at=future, role=ACTION_EDIT)
|
|
203
|
+
)
|
|
204
|
+
assert renewed.id == share.id
|
|
205
|
+
assert await _visible(service.db, FRIEND, ACTION_EDIT) == ["n"]
|
|
206
|
+
assert len(await service.for_resource(_who(OWNER), NOTE_TYPE, note_id)) == 1
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Tenant deletion purges only the deleted tenant through a durable handler."""
|
|
2
|
+
|
|
3
|
+
from sharing.models import ResourceShare
|
|
4
|
+
from sharing.module import SharingModule
|
|
5
|
+
from simple_module_core.durable_events import DurableContext, event_key
|
|
6
|
+
from simple_module_core.events import EventBus
|
|
7
|
+
from simple_module_core.tenant_events import TenantDeleted
|
|
8
|
+
from simple_module_db.tenancy import all_tenants, tenant_context
|
|
9
|
+
from sqlmodel import select
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def test_durable_tenant_cleanup_is_scoped_idempotent_and_transactional(db_session):
|
|
13
|
+
bus = EventBus()
|
|
14
|
+
SharingModule().register_event_handlers(bus)
|
|
15
|
+
names = bus.durable.handler_names(TenantDeleted)
|
|
16
|
+
assert len(names) == 1
|
|
17
|
+
subscription = bus.durable.lookup(event_key(TenantDeleted), names[0])
|
|
18
|
+
assert subscription is not None
|
|
19
|
+
for tenant_id in ("deleted-tenant", "other-tenant"):
|
|
20
|
+
with tenant_context(tenant_id):
|
|
21
|
+
db_session.add(
|
|
22
|
+
ResourceShare(
|
|
23
|
+
resource_type="test.note",
|
|
24
|
+
resource_id="note",
|
|
25
|
+
subject_id="user",
|
|
26
|
+
role="view",
|
|
27
|
+
granted_by="owner",
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
await db_session.flush()
|
|
31
|
+
await db_session.commit()
|
|
32
|
+
event = TenantDeleted(tenant_id="deleted-tenant", slug="deleted", name="Deleted")
|
|
33
|
+
ctx = DurableContext(db=db_session, attempt=1)
|
|
34
|
+
# A worker rollback must restore rows: the handler must never commit its session.
|
|
35
|
+
await subscription.handler(event, ctx)
|
|
36
|
+
await db_session.rollback()
|
|
37
|
+
with all_tenants():
|
|
38
|
+
assert len((await db_session.execute(select(ResourceShare))).scalars().all()) == 2
|
|
39
|
+
await subscription.handler(event, ctx)
|
|
40
|
+
await subscription.handler(event, DurableContext(db=db_session, attempt=2))
|
|
41
|
+
with all_tenants():
|
|
42
|
+
remaining = (await db_session.execute(select(ResourceShare))).scalars().all()
|
|
43
|
+
assert [row.tenant_id for row in remaining] == ["other-tenant"]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { configureI18n, keys } from '@simple-module-py/i18n';
|
|
2
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
configureI18n({ locale: 'en', messages: {} });
|
|
7
|
+
const { post, get } = vi.hoisted(() => ({ post: vi.fn(), get: vi.fn() }));
|
|
8
|
+
vi.mock('@inertiajs/react', () => ({
|
|
9
|
+
Head: () => null,
|
|
10
|
+
router: { post, get },
|
|
11
|
+
usePage: () => ({ props: {} }),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import Admin from '../sharing/pages/Admin';
|
|
15
|
+
|
|
16
|
+
const props: ComponentProps<typeof Admin> = {
|
|
17
|
+
shares: [
|
|
18
|
+
{
|
|
19
|
+
id: 'share-1',
|
|
20
|
+
resource_type: 'notes.note',
|
|
21
|
+
resource_id: 'note-1',
|
|
22
|
+
subject_type: 'user',
|
|
23
|
+
subject_id: 'friend',
|
|
24
|
+
subject_label: 'Friend',
|
|
25
|
+
role: 'view',
|
|
26
|
+
granted_by: 'owner',
|
|
27
|
+
granted_by_label: 'Owner',
|
|
28
|
+
expires_at: null,
|
|
29
|
+
expired: false,
|
|
30
|
+
created_at: null,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
total: 21,
|
|
34
|
+
page: 2,
|
|
35
|
+
per_page: 10,
|
|
36
|
+
can_revoke: true,
|
|
37
|
+
tenant_required: false,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
afterEach(() => vi.clearAllMocks());
|
|
41
|
+
|
|
42
|
+
describe('sharing Admin', () => {
|
|
43
|
+
it('renders labels and hides revoke from read-only viewers', () => {
|
|
44
|
+
render(<Admin {...props} can_revoke={false} />);
|
|
45
|
+
expect(screen.getByText('Friend')).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByText('Owner')).toBeInTheDocument();
|
|
47
|
+
expect(screen.queryByRole('button', { name: keys.sharing.list.revoke })).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('shows the tenant warning instead of shares and pagination when unbound', () => {
|
|
51
|
+
render(<Admin {...props} tenant_required />);
|
|
52
|
+
expect(screen.getByText(keys.sharing.admin.tenant_required)).toBeInTheDocument();
|
|
53
|
+
expect(screen.queryByText('Friend')).toBeNull();
|
|
54
|
+
expect(screen.queryByRole('button', { name: keys.ui.pagination.next })).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('shows an empty state without pagination for an empty list', () => {
|
|
58
|
+
render(<Admin {...props} shares={[]} total={0} page={1} />);
|
|
59
|
+
expect(screen.getByText(keys.sharing.list.empty_title)).toBeInTheDocument();
|
|
60
|
+
expect(screen.queryByRole('button', { name: keys.ui.pagination.next })).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it.each([
|
|
64
|
+
[keys.ui.pagination.previous, 1],
|
|
65
|
+
[keys.ui.pagination.next, 3],
|
|
66
|
+
])('navigates using %s', (name, page) => {
|
|
67
|
+
render(<Admin {...props} />);
|
|
68
|
+
fireEvent.click(screen.getByRole('button', { name }));
|
|
69
|
+
expect(get).toHaveBeenCalledWith('/admin/sharing/', { page }, { preserveScroll: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it.each([1, 3])('disables navigation beyond page %s', (page) => {
|
|
73
|
+
render(<Admin {...props} page={page} />);
|
|
74
|
+
const name = page === 1 ? keys.ui.pagination.previous : keys.ui.pagination.next;
|
|
75
|
+
expect(screen.getByRole('button', { name })).toBeDisabled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('cancels without writing and revokes only after confirmation', async () => {
|
|
79
|
+
render(<Admin {...props} />);
|
|
80
|
+
fireEvent.click(screen.getByRole('button', { name: keys.sharing.list.revoke }));
|
|
81
|
+
expect(post).not.toHaveBeenCalled();
|
|
82
|
+
fireEvent.click(await screen.findByRole('button', { name: keys.sharing.revoke.cancel }));
|
|
83
|
+
await waitFor(() => expect(screen.queryByText(keys.sharing.revoke.description)).toBeNull());
|
|
84
|
+
expect(post).not.toHaveBeenCalled();
|
|
85
|
+
fireEvent.click(screen.getByRole('button', { name: keys.sharing.list.revoke }));
|
|
86
|
+
fireEvent.click(await screen.findByRole('button', { name: keys.sharing.revoke.confirm }));
|
|
87
|
+
expect(post).toHaveBeenCalledWith(
|
|
88
|
+
'/admin/sharing/share-1/revoke',
|
|
89
|
+
{},
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
preserveScroll: true,
|
|
92
|
+
onFinish: expect.any(Function),
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
expect(screen.getByRole('button', { name: keys.sharing.revoke.confirm })).toBeDisabled();
|
|
96
|
+
act(() => post.mock.calls[0][2].onFinish());
|
|
97
|
+
await waitFor(() => expect(screen.queryByText(keys.sharing.revoke.description)).toBeNull());
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('clears pending state on visit completion so an unchanged share can be retried', async () => {
|
|
101
|
+
render(<Admin {...props} />);
|
|
102
|
+
fireEvent.click(screen.getByRole('button', { name: keys.sharing.list.revoke }));
|
|
103
|
+
fireEvent.click(await screen.findByRole('button', { name: keys.sharing.revoke.confirm }));
|
|
104
|
+
act(() => post.mock.calls[0][2].onFinish());
|
|
105
|
+
await waitFor(() => expect(screen.queryByText(keys.sharing.revoke.description)).toBeNull());
|
|
106
|
+
expect(screen.getByText('Friend')).toBeInTheDocument();
|
|
107
|
+
fireEvent.click(screen.getByRole('button', { name: keys.sharing.list.revoke }));
|
|
108
|
+
expect(await screen.findByRole('button', { name: keys.sharing.revoke.confirm })).toBeEnabled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { configureI18n } from '@simple-module-py/i18n';
|
|
2
|
+
import { act, fireEvent, render, screen, within } from '@testing-library/react';
|
|
3
|
+
import { toast } from 'sonner';
|
|
4
|
+
import { beforeEach, expect, test, vi } from 'vitest';
|
|
5
|
+
import Admin from '../sharing/pages/Admin';
|
|
6
|
+
|
|
7
|
+
configureI18n({ locale: 'en', messages: {} });
|
|
8
|
+
const { post } = vi.hoisted(() => ({ post: vi.fn() }));
|
|
9
|
+
vi.mock('@inertiajs/react', () => ({
|
|
10
|
+
Head: () => null,
|
|
11
|
+
router: { post },
|
|
12
|
+
usePage: () => ({ props: {} }),
|
|
13
|
+
}));
|
|
14
|
+
vi.mock('sonner', () => ({ toast: { error: vi.fn() } }));
|
|
15
|
+
|
|
16
|
+
beforeEach(() => vi.clearAllMocks());
|
|
17
|
+
|
|
18
|
+
test.each(['onError', 'onHttpException', 'onNetworkError'])(
|
|
19
|
+
'revoke failures show a toast and release the busy dialog (%s)',
|
|
20
|
+
(callback) => {
|
|
21
|
+
render(
|
|
22
|
+
<Admin
|
|
23
|
+
shares={[
|
|
24
|
+
{
|
|
25
|
+
id: 's1',
|
|
26
|
+
resource_type: 'order',
|
|
27
|
+
resource_id: 'o1',
|
|
28
|
+
subject_type: 'user',
|
|
29
|
+
subject_id: 'u1',
|
|
30
|
+
subject_label: null,
|
|
31
|
+
role: 'viewer',
|
|
32
|
+
granted_by: 'u2',
|
|
33
|
+
granted_by_label: null,
|
|
34
|
+
expires_at: null,
|
|
35
|
+
expired: false,
|
|
36
|
+
created_at: null,
|
|
37
|
+
},
|
|
38
|
+
]}
|
|
39
|
+
total={1}
|
|
40
|
+
page={1}
|
|
41
|
+
per_page={20}
|
|
42
|
+
can_revoke
|
|
43
|
+
tenant_required={false}
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
fireEvent.click(screen.getAllByRole('button', { name: 'sharing.list.revoke' })[0]);
|
|
47
|
+
fireEvent.click(
|
|
48
|
+
within(screen.getByRole('alertdialog')).getByRole('button', {
|
|
49
|
+
name: 'sharing.revoke.confirm',
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
const [url, , options] = post.mock.calls[0];
|
|
53
|
+
expect(url).toBe('/admin/sharing/s1/revoke');
|
|
54
|
+
act(() => {
|
|
55
|
+
options[callback]({});
|
|
56
|
+
options.onFinish();
|
|
57
|
+
});
|
|
58
|
+
expect(toast.error).toHaveBeenCalledWith('sharing.revoke.failed');
|
|
59
|
+
expect(screen.queryByRole('alertdialog')).toBeNull();
|
|
60
|
+
},
|
|
61
|
+
);
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_sharing-0.0.38 → simple_module_sharing-0.0.39}/tests/test_sharing_tenant_audience.py
RENAMED
|
File without changes
|
|
File without changes
|