Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,19 @@ def mock(*args, **kwargs):


@pytest.fixture
def capture_and_mock_requests(monkeypatch):
def inner():
captured_requests = []
def capture_and_mock_request(monkeypatch):
def inner(method, response_dict, status_code):
request_args = []
request_kwargs = {}

def capture(*args, **kwargs):
captured_requests.append((args, kwargs))
return MockResponse({}, 200)
def capture_and_mock(*args, **kwargs):
request_args.extend(args)
request_kwargs.update(kwargs)

monkeypatch.setattr(requests, "get", capture)
monkeypatch.setattr(requests, "post", capture)
return MockResponse(response_dict, status_code)

return captured_requests
monkeypatch.setattr(requests, method, capture_and_mock)

return (request_args, request_kwargs)

return inner
161 changes: 157 additions & 4 deletions tests/test_audit_trail.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,18 @@ def test_create_audit_trail_event_succeeds(self, mock_request_method):
"actor_id": "user_12345",
"target_name": "Ryota Yamasato",
"target_id": "user_67890",
"occurred_at": datetime.utcnow().isoformat(),
"occurred_at": datetime.now().isoformat(),
"metadata": {"a": "b"},
}
mock_response = Response()
mock_response.status_code = 200
mock_request_method("post", mock_response, 200)
response = self.audit_trail.create_event(event)
assert response.status_code == 200

result = self.audit_trail.create_event(event)
assert result == True

def test_create_audit_trail_event_fails_with_long_metadata(self):
with pytest.raises(Exception, match=r"Number of metadata keys exceeds .*"):
with pytest.raises(ValueError, match=r"Number of metadata keys exceeds .*"):
metadata = {str(num): num for num in range(51)}
event = {
"group": "Terrace House",
Expand All @@ -48,3 +49,155 @@ def test_create_audit_trail_event_fails_with_long_metadata(self):
"metadata": metadata,
}
self.audit_trail.create_event(event)

def test_get_events_succeeds(self, mock_request_method):
event = {
"id": "evt_123",
"group": "Terrace House",
"location": "1.1.1.1",
"latitude": None,
"longitude": None,
"action": {
"id": "evt_action_123",
"name": "house.created",
"project_id": "project_123",
},
"type": "C",
"actor_name": "Daiki Miyagi",
"actor_id": "user_12345",
"target_name": "Ryota Yamasato",
"target_id": "user_67890",
"occurred_at": datetime.now().isoformat(),
"metadata": {"a": "b"},
}

response = {
"data": [event,],
"listMetadata": {"before": None, "after": None,},
}
mock_request_method("get", response, 200)

events, before, after = self.audit_trail.get_events()
assert events[0].to_dict() == event

def test_get_events_raises_valueerror_when_before_and_after_provided(self):
with pytest.raises(ValueError):
self.audit_trail.get_events(before="evt_123", after="evt_456")

def test_get_events_correctly_includes_occured_at_filter(
self, capture_and_mock_request
):
event = {
"id": "evt_123",
"group": "Terrace House",
"location": "1.1.1.1",
"latitude": None,
"longitude": None,
"action": {
"id": "evt_action_123",
"name": "house.created",
"project_id": "project_123",
},
"type": "C",
"actor_name": "Daiki Miyagi",
"actor_id": "user_12345",
"target_name": "Ryota Yamasato",
"target_id": "user_67890",
"occurred_at": datetime.now().isoformat(),
"metadata": {"a": "b"},
}

response = {
"data": [event,],
"listMetadata": {"before": None, "after": None,},
}
request_args, request_kwargs = capture_and_mock_request("get", response, 200)

self.audit_trail.get_events(
occurred_at=datetime.now(),
occurred_at_gte=datetime.now(),
occurred_at_gt=datetime.now(),
occurred_at_lte=datetime.now,
occurred_at_lt=datetime.now(),
)

request_params = request_kwargs["params"]
assert "occurred_at" in request_params
assert "occurred_at_gte" not in request_params
assert "occurred_at_gt" not in request_params
assert "occurred_at_lte" not in request_params
assert "occurred_at_lt" not in request_params

def test_get_events_correctly_includes_occurred_at_gte(
self, capture_and_mock_request
):
event = {
"id": "evt_123",
"group": "Terrace House",
"location": "1.1.1.1",
"latitude": None,
"longitude": None,
"action": {
"id": "evt_action_123",
"name": "house.created",
"project_id": "project_123",
},
"type": "C",
"actor_name": "Daiki Miyagi",
"actor_id": "user_12345",
"target_name": "Ryota Yamasato",
"target_id": "user_67890",
"occurred_at": datetime.now().isoformat(),
"metadata": {"a": "b"},
}

response = {
"data": [event,],
"listMetadata": {"before": None, "after": None,},
}
request_args, request_kwargs = capture_and_mock_request("get", response, 200)

self.audit_trail.get_events(
occurred_at_gte=datetime.now(), occurred_at_gt=datetime.now(),
)

request_params = request_kwargs["params"]
assert "occurred_at_gte" in request_params
assert "occurred_at_gt" not in request_params

def test_get_events_correctly_includes_occured_at_lte(
self, capture_and_mock_request
):
event = {
"id": "evt_123",
"group": "Terrace House",
"location": "1.1.1.1",
"latitude": None,
"longitude": None,
"action": {
"id": "evt_action_123",
"name": "house.created",
"project_id": "project_123",
},
"type": "C",
"actor_name": "Daiki Miyagi",
"actor_id": "user_12345",
"target_name": "Ryota Yamasato",
"target_id": "user_67890",
"occurred_at": datetime.now().isoformat(),
"metadata": {"a": "b"},
}

response = {
"data": [event,],
"listMetadata": {"before": None, "after": None,},
}
request_args, request_kwargs = capture_and_mock_request("get", response, 200)

self.audit_trail.get_events(
occurred_at_lte=datetime.now, occurred_at_lt=datetime.now()
)

request_params = request_kwargs["params"]
assert "occurred_at_lte" in request_params
assert "occurred_at_lt" not in request_params
8 changes: 3 additions & 5 deletions tests/utils/test_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,12 @@ def test_request_bad_body_raises_expected_exception_with_request_data(
# This'll fail for sure here but... just using the nice error that'd come up
assert ex.__class__ == ServerException

def test_request_includes_base_headers(self, capture_and_mock_requests):
requests = capture_and_mock_requests()
def test_request_includes_base_headers(self, capture_and_mock_request):
request_args, request_kwargs = capture_and_mock_request("get", {}, 200)

RequestHelper().request("ok_place")

assert len(requests) == 1

base_headers = set(BASE_HEADERS.items())
headers = set(requests[0][1]["headers"].items())
headers = set(request_kwargs["headers"].items())

assert base_headers.issubset(headers)
2 changes: 1 addition & 1 deletion workos/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

__package_url__ = "https://github.com/workos-inc/workos-python"

__version__ = "0.3.3"
__version__ = "0.4.0"

__author__ = "WorkOS"

Expand Down
115 changes: 111 additions & 4 deletions workos/audit_trail.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import workos
from workos.exceptions import ConfigurationException
from workos.utils.request import RequestHelper, REQUEST_METHOD_POST
from workos.resources.event import WorkOSEvent
from workos.utils.request import RequestHelper, REQUEST_METHOD_GET, REQUEST_METHOD_POST
from workos.utils.validation import AUDIT_TRAIL_MODULE, validate_settings

EVENTS_PATH = "events"
METADATA_LIMIT = 50
DEFAULT_EVENT_LIMIT = 10


class AuditTrail(object):
Expand Down Expand Up @@ -48,21 +50,126 @@ def create_event(self, event, idempotency_key=None):
idempotency_key (str) - An idempotency key

Returns:
dict: Response from WorkOS
boolean: Returns True
"""
if len(event.get("metadata", {})) > METADATA_LIMIT:
raise Exception(
raise ValueError(
"Number of metadata keys exceeds {}.".format(METADATA_LIMIT)
)

headers = {
"idempotency-key": idempotency_key,
}

return self.request_helper.request(
self.request_helper.request(
EVENTS_PATH,
method=REQUEST_METHOD_POST,
params=event,
headers=headers,
token=workos.api_key,
)

return True

def get_events(
self,
before=None,
after=None,
limit=DEFAULT_EVENT_LIMIT,
group=None,
action=None,
action_type=None,
actor_name=None,
actor_id=None,
target_name=None,
target_id=None,
occurred_at=None,
occurred_at_gt=None,
occurred_at_gte=None,
occurred_at_lt=None,
occurred_at_lte=None,
search=None,
):
"""Filter for Audit Trail Events.

Kwargs:
before (str) - Event ID to look before
after (str) - Event ID to look after
limit (int) - Number of Events to return
group (str|list) - Group or groups to filter for
action (str|list) - Action or actions to filter for
action_type (str|list) - Action type or types to filter for
actor_name (str|list) - Actor name or name to filter for
actor_id (str|list) - Actor ID or IDs to filter for
target_name (str|list) - Target name or names to filter for
target_id (str|list) - Target ID or IDs to filter for
occurred_at (str) - ISO-8601 datetime of when an event occurred
occurred_at_gt (str) - ISO-8601 datetime of when an event occurred after
occurred_at_gte (str) - ISO-8601 datetime of when an event occurred at or after
occurred_at_lt (str) - ISO-8601 datetime of when an event occurred before
occurred_at_lte (str) - ISO-8601 datetime of when an event occured at or before
search (str) - Keyword search

Returns:
tuple
list - List of WorkOSEvent objects
string - Event ID to use as before cursor
string - Event ID to use as after cursor
"""
if before and after:
raise ValueError("Specify either before or after")

params = {
"before": before,
"after": after,
"limit": limit,
}

if group is not None:
params["group"] = list(group)

if action is not None:
params["action"] = list(action)

if action_type is not None:
params["action_type"] = list(action_type)

if actor_name is not None:
params["actor_name"] = list(actor_name)

if actor_id is not None:
params["actor_id"] = list(actor_id)

if target_name is not None:
params["target_name"] = list(target_name)

if target_id is not None:
params["target_id"] = list(target_id)

if occurred_at is not None:
params["occurred_at"] = occurred_at
else:
if occurred_at_gte is not None:
params["occurred_at_gte"] = occurred_at_gte
elif occurred_at_gt is not None:
params["occurred_at_gt"] = occurred_at_gt

if occurred_at_lte is not None:
params["occurred_at_lte"] = occurred_at_lte
elif occurred_at_lt is not None:
params["occurred_at_lt"] = occurred_at_lt

if search is not None:
params["search"] = search

response = self.request_helper.request(
EVENTS_PATH, method=REQUEST_METHOD_GET, params=params, token=workos.api_key,
)

events = [
WorkOSEvent.construct_from_response(data) for data in response["data"]
]
before = response["listMetadata"]["before"]
after = response["listMetadata"]["after"]

return (events, before, after)
Loading