Skip to content

Commit

Permalink
Invalidate safe apps for removed chains (#1020)
Browse files Browse the repository at this point in the history
- Decouples `post_delete` signal from the affected flow, as it remains as it is.
- Change the handling of `post_save` signal to handle `pre_save` signal, to have access to the instance's `Chain` list before the update is made.
- Both the `Chain` items related to the `SafeApp` before and after the update are stored in a `Set` to avoid repetitions. Hooks are dispatched for all the `Chain` items in the set.
  • Loading branch information
hectorgomezv authored Jan 11, 2024
1 parent 27a5c43 commit f263444
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 3 deletions.
29 changes: 26 additions & 3 deletions src/safe_apps/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@

from django.conf import settings
from django.core.cache import caches
from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
from django.db.models.signals import (
m2m_changed,
post_delete,
post_save,
pre_delete,
pre_save,
)
from django.dispatch import receiver

from clients.safe_client_gateway import HookEvent, flush, hook_event
Expand All @@ -13,9 +19,26 @@
logger = logging.getLogger(__name__)


@receiver(post_save, sender=SafeApp)
@receiver(post_delete, sender=SafeApp)
@receiver(pre_save, sender=SafeApp)
def on_safe_app_update(sender: SafeApp, instance: SafeApp, **kwargs: Any) -> None:
logger.info("Clearing safe-apps cache")
caches["safe-apps"].clear()
if settings.FF_HOOK_EVENTS:
chain_ids = set(instance.chain_ids)
if instance.app_id is not None: # existing SafeApp being updated
previous = SafeApp.objects.filter(app_id=instance.app_id).first()
if previous is not None:
chain_ids.update(previous.chain_ids)
for chain_id in chain_ids:
hook_event(
HookEvent(type=HookEvent.Type.SAFE_APPS_UPDATE, chain_id=chain_id)
)
else:
flush()


@receiver(post_delete, sender=SafeApp)
def on_safe_app_delete(sender: SafeApp, instance: SafeApp, **kwargs: Any) -> None:
logger.info("Clearing safe-apps cache")
caches["safe-apps"].clear()
if settings.FF_HOOK_EVENTS:
Expand Down
146 changes: 146 additions & 0 deletions src/safe_apps/tests/test_signals_ff_hook_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,152 @@ def test_on_safe_app_update(self) -> None:
== "Basic example-token"
)

@responses.activate
def test_on_safe_app_update_by_adding_chain_ids(self) -> None:
responses.add(
responses.POST,
"http://127.0.0.1/v1/hooks/events",
status=200,
match=[
responses.matchers.header_matcher(
{"Authorization": "Basic example-token"}
),
responses.matchers.json_params_matcher(
{"type": "SAFE_APPS_UPDATE", "chainId": "1"}
),
],
)

safe_app = SafeApp(app_id=1, chain_ids=[1])
safe_app.save() # create
safe_app.chain_ids = [1, 2, 3]
safe_app.save() # update

assert len(responses.calls) == 4
assert isinstance(responses.calls[0], responses.Call)
assert (
responses.calls[0].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "1"}'
)
assert responses.calls[0].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[0].request.headers.get("Authorization")
== "Basic example-token"
)
assert isinstance(responses.calls[1], responses.Call)
assert (
responses.calls[1].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "1"}'
)
assert responses.calls[1].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[1].request.headers.get("Authorization")
== "Basic example-token"
)
assert isinstance(responses.calls[2], responses.Call)
assert (
responses.calls[2].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "2"}'
)
assert responses.calls[2].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[2].request.headers.get("Authorization")
== "Basic example-token"
)
assert isinstance(responses.calls[3], responses.Call)
assert (
responses.calls[3].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "3"}'
)
assert responses.calls[3].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[3].request.headers.get("Authorization")
== "Basic example-token"
)

@responses.activate
def test_on_safe_app_update_by_removing_chain_ids(self) -> None:
responses.add(
responses.POST,
"http://127.0.0.1/v1/hooks/events",
status=200,
match=[
responses.matchers.header_matcher(
{"Authorization": "Basic example-token"}
),
responses.matchers.json_params_matcher(
{"type": "SAFE_APPS_UPDATE", "chainId": "1"}
),
],
)

safe_app = SafeApp(app_id=1, chain_ids=[1, 2, 3])
safe_app.save() # create
safe_app.chain_ids = [1]
safe_app.save() # update

assert len(responses.calls) == 6
assert isinstance(responses.calls[0], responses.Call)
assert (
responses.calls[0].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "1"}'
)
assert responses.calls[0].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[0].request.headers.get("Authorization")
== "Basic example-token"
)
assert isinstance(responses.calls[1], responses.Call)
assert (
responses.calls[1].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "2"}'
)
assert responses.calls[1].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[1].request.headers.get("Authorization")
== "Basic example-token"
)
assert isinstance(responses.calls[2], responses.Call)
assert (
responses.calls[2].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "3"}'
)
assert responses.calls[2].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[2].request.headers.get("Authorization")
== "Basic example-token"
)
assert isinstance(responses.calls[3], responses.Call)
assert (
responses.calls[3].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "1"}'
)
assert responses.calls[3].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[3].request.headers.get("Authorization")
== "Basic example-token"
)
assert isinstance(responses.calls[4], responses.Call)
assert (
responses.calls[4].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "2"}'
)
assert responses.calls[4].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[4].request.headers.get("Authorization")
== "Basic example-token"
)
assert isinstance(responses.calls[5], responses.Call)
assert (
responses.calls[5].request.body
== b'{"type": "SAFE_APPS_UPDATE", "chainId": "3"}'
)
assert responses.calls[5].request.url == "http://127.0.0.1/v1/hooks/events"
assert (
responses.calls[5].request.headers.get("Authorization")
== "Basic example-token"
)

@responses.activate
def test_on_safe_app_delete(self) -> None:
responses.add(
Expand Down

0 comments on commit f263444

Please sign in to comment.