Skip to content

Commit 0684850

Browse files
authored
Merge branch 'main' into password-hash-types
2 parents d8931ac + 7aebfdd commit 0684850

30 files changed

+799
-11
lines changed

context7.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"url": "https://context7.com/workos/workos-python",
3+
"public_key": "pk_q7NnKuFFXMWA7WnmjMHQU"
4+
}

tests/test_api_keys.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# type: ignore
2+
import pytest
3+
4+
from tests.utils.fixtures.mock_api_key import MockApiKey
5+
from tests.utils.syncify import syncify
6+
from workos.api_keys import API_KEY_VALIDATION_PATH, ApiKeys, AsyncApiKeys
7+
8+
9+
@pytest.mark.sync_and_async(ApiKeys, AsyncApiKeys)
10+
class TestApiKeys:
11+
@pytest.fixture
12+
def mock_api_key(self):
13+
return MockApiKey().dict()
14+
15+
@pytest.fixture
16+
def api_key(self):
17+
return "sk_my_api_key"
18+
19+
def test_validate_api_key_with_valid_key(
20+
self,
21+
module_instance,
22+
api_key,
23+
mock_api_key,
24+
capture_and_mock_http_client_request,
25+
):
26+
response_body = {"api_key": mock_api_key}
27+
request_kwargs = capture_and_mock_http_client_request(
28+
module_instance._http_client, response_body, 200
29+
)
30+
31+
api_key_details = syncify(module_instance.validate_api_key(value=api_key))
32+
33+
assert request_kwargs["url"].endswith(API_KEY_VALIDATION_PATH)
34+
assert request_kwargs["method"] == "post"
35+
assert api_key_details.id == mock_api_key["id"]
36+
assert api_key_details.name == mock_api_key["name"]
37+
assert api_key_details.object == "api_key"
38+
39+
def test_validate_api_key_with_invalid_key(
40+
self,
41+
module_instance,
42+
mock_http_client_with_response,
43+
):
44+
mock_http_client_with_response(
45+
module_instance._http_client,
46+
{"api_key": None},
47+
200,
48+
)
49+
50+
assert syncify(module_instance.validate_api_key(value="invalid-key")) is None

tests/test_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def test_client_with_api_key_and_client_id_environment_variables(self):
3737
os.environ.pop("WORKOS_API_KEY")
3838
os.environ.pop("WORKOS_CLIENT_ID")
3939

40+
def test_initialize_api_keys(self, default_client):
41+
assert bool(default_client.api_keys)
42+
4043
def test_initialize_sso(self, default_client):
4144
assert bool(default_client.sso)
4245

@@ -112,6 +115,9 @@ def test_client_with_api_key_and_client_id_environment_variables(self):
112115
os.environ.pop("WORKOS_API_KEY")
113116
os.environ.pop("WORKOS_CLIENT_ID")
114117

118+
def test_initialize_api_keys(self, default_client):
119+
assert bool(default_client.api_keys)
120+
115121
def test_initialize_directory_sync(self, default_client):
116122
assert bool(default_client.directory_sync)
117123

tests/test_pipes.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import pytest
2+
3+
from tests.utils.syncify import syncify
4+
from workos.pipes import AsyncPipes, Pipes
5+
6+
7+
@pytest.mark.sync_and_async(Pipes, AsyncPipes)
8+
class TestPipes:
9+
@pytest.fixture
10+
def mock_access_token(self):
11+
return {
12+
"object": "access_token",
13+
"access_token": "test_access_token_123",
14+
"expires_at": "2026-01-09T12:00:00.000Z",
15+
"scopes": ["read:users", "write:users"],
16+
"missing_scopes": [],
17+
}
18+
19+
def test_get_access_token_success_with_expiry(
20+
self,
21+
module_instance,
22+
mock_access_token,
23+
capture_and_mock_http_client_request,
24+
):
25+
response_body = {
26+
"active": True,
27+
"access_token": mock_access_token,
28+
}
29+
request_kwargs = capture_and_mock_http_client_request(
30+
module_instance._http_client, response_body, 200
31+
)
32+
33+
result = syncify(
34+
module_instance.get_access_token(
35+
provider="test-provider",
36+
user_id="user_123",
37+
)
38+
)
39+
40+
assert request_kwargs["url"].endswith("data-integrations/test-provider/token")
41+
assert request_kwargs["method"] == "post"
42+
assert request_kwargs["json"]["user_id"] == "user_123"
43+
assert result.active is True
44+
assert result.access_token.access_token == mock_access_token["access_token"]
45+
assert result.access_token.scopes == mock_access_token["scopes"]
46+
47+
def test_get_access_token_success_without_expiry(
48+
self,
49+
module_instance,
50+
capture_and_mock_http_client_request,
51+
):
52+
response_body = {
53+
"active": True,
54+
"access_token": {
55+
"object": "access_token",
56+
"access_token": "test_token",
57+
"expires_at": None,
58+
"scopes": ["read"],
59+
"missing_scopes": [],
60+
},
61+
}
62+
capture_and_mock_http_client_request(
63+
module_instance._http_client, response_body, 200
64+
)
65+
66+
result = syncify(
67+
module_instance.get_access_token(
68+
provider="test-provider",
69+
user_id="user_123",
70+
)
71+
)
72+
73+
assert result.active is True
74+
assert result.access_token.expires_at is None
75+
76+
def test_get_access_token_with_organization_id(
77+
self,
78+
module_instance,
79+
mock_access_token,
80+
capture_and_mock_http_client_request,
81+
):
82+
response_body = {
83+
"active": True,
84+
"access_token": mock_access_token,
85+
}
86+
request_kwargs = capture_and_mock_http_client_request(
87+
module_instance._http_client, response_body, 200
88+
)
89+
90+
syncify(
91+
module_instance.get_access_token(
92+
provider="test-provider",
93+
user_id="user_123",
94+
organization_id="org_456",
95+
)
96+
)
97+
98+
assert request_kwargs["json"]["organization_id"] == "org_456"
99+
100+
def test_get_access_token_without_organization_id(
101+
self,
102+
module_instance,
103+
mock_access_token,
104+
capture_and_mock_http_client_request,
105+
):
106+
response_body = {
107+
"active": True,
108+
"access_token": mock_access_token,
109+
}
110+
request_kwargs = capture_and_mock_http_client_request(
111+
module_instance._http_client, response_body, 200
112+
)
113+
114+
syncify(
115+
module_instance.get_access_token(
116+
provider="test-provider",
117+
user_id="user_123",
118+
)
119+
)
120+
121+
assert "organization_id" not in request_kwargs["json"]
122+
123+
def test_get_access_token_not_installed(
124+
self,
125+
module_instance,
126+
capture_and_mock_http_client_request,
127+
):
128+
response_body = {
129+
"active": False,
130+
"error": "not_installed",
131+
}
132+
capture_and_mock_http_client_request(
133+
module_instance._http_client, response_body, 200
134+
)
135+
136+
result = syncify(
137+
module_instance.get_access_token(
138+
provider="test-provider",
139+
user_id="user_123",
140+
)
141+
)
142+
143+
assert result.active is False
144+
assert result.error == "not_installed"
145+
146+
def test_get_access_token_needs_reauthorization(
147+
self,
148+
module_instance,
149+
capture_and_mock_http_client_request,
150+
):
151+
response_body = {
152+
"active": False,
153+
"error": "needs_reauthorization",
154+
}
155+
capture_and_mock_http_client_request(
156+
module_instance._http_client, response_body, 200
157+
)
158+
159+
result = syncify(
160+
module_instance.get_access_token(
161+
provider="test-provider",
162+
user_id="user_123",
163+
)
164+
)
165+
166+
assert result.active is False
167+
assert result.error == "needs_reauthorization"

tests/test_sso.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ def mock_magic_link_profile(self):
3030
first_name=None,
3131
last_name=None,
3232
role=None,
33+
roles=None,
3334
groups=None,
3435
raw_attributes={},
3536
).dict()

tests/test_user_management.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from tests.utils.fixtures.mock_auth_factor_totp import MockAuthenticationFactorTotp
88
from tests.utils.fixtures.mock_email_verification import MockEmailVerification
9+
from tests.utils.fixtures.mock_feature_flag import MockFeatureFlag
910
from tests.utils.fixtures.mock_invitation import MockInvitation
1011
from tests.utils.fixtures.mock_magic_auth import MockMagicAuth
1112
from tests.utils.fixtures.mock_organization_membership import MockOrganizationMembership
@@ -146,6 +147,14 @@ def mock_invitations_multiple_pages(self):
146147
invitations_list = [MockInvitation(id=str(i)).dict() for i in range(40)]
147148
return list_response_of(data=invitations_list)
148149

150+
@pytest.fixture
151+
def mock_feature_flags(self):
152+
return {
153+
"data": [MockFeatureFlag(id=f"flag_{str(i)}").dict() for i in range(2)],
154+
"object": "list",
155+
"list_metadata": {"before": None, "after": None},
156+
}
157+
149158

150159
class TestUserManagementBase(UserManagementFixtures):
151160
@pytest.fixture(autouse=True)
@@ -1194,3 +1203,84 @@ def test_revoke_invitation(
11941203
)
11951204
assert request_kwargs["method"] == "post"
11961205
assert isinstance(invitation, Invitation)
1206+
1207+
def test_resend_invitation(
1208+
self, capture_and_mock_http_client_request, mock_invitation
1209+
):
1210+
request_kwargs = capture_and_mock_http_client_request(
1211+
self.http_client, mock_invitation, 200
1212+
)
1213+
1214+
invitation = syncify(self.user_management.resend_invitation("invitation_ABCDE"))
1215+
1216+
assert request_kwargs["url"].endswith(
1217+
"user_management/invitations/invitation_ABCDE/resend"
1218+
)
1219+
assert request_kwargs["method"] == "post"
1220+
assert isinstance(invitation, Invitation)
1221+
assert invitation.id == "invitation_ABCDE"
1222+
1223+
def test_resend_invitation_not_found(self, capture_and_mock_http_client_request):
1224+
error_response = {
1225+
"message": "Invitation not found",
1226+
"code": "not_found",
1227+
}
1228+
capture_and_mock_http_client_request(self.http_client, error_response, 404)
1229+
1230+
with pytest.raises(Exception):
1231+
syncify(self.user_management.resend_invitation("invitation_nonexistent"))
1232+
1233+
def test_resend_invitation_expired(self, capture_and_mock_http_client_request):
1234+
error_response = {
1235+
"message": "Invite has expired.",
1236+
"code": "invite_expired",
1237+
}
1238+
capture_and_mock_http_client_request(self.http_client, error_response, 400)
1239+
1240+
with pytest.raises(Exception):
1241+
syncify(self.user_management.resend_invitation("invitation_expired"))
1242+
1243+
def test_resend_invitation_revoked(self, capture_and_mock_http_client_request):
1244+
error_response = {
1245+
"message": "Invite has been revoked.",
1246+
"code": "invite_revoked",
1247+
}
1248+
capture_and_mock_http_client_request(self.http_client, error_response, 400)
1249+
1250+
with pytest.raises(Exception):
1251+
syncify(self.user_management.resend_invitation("invitation_revoked"))
1252+
1253+
def test_resend_invitation_accepted(self, capture_and_mock_http_client_request):
1254+
error_response = {
1255+
"message": "Invite has already been accepted.",
1256+
"code": "invite_accepted",
1257+
}
1258+
capture_and_mock_http_client_request(self.http_client, error_response, 400)
1259+
1260+
with pytest.raises(Exception):
1261+
syncify(self.user_management.resend_invitation("invitation_accepted"))
1262+
1263+
def test_list_feature_flags(
1264+
self, mock_feature_flags, capture_and_mock_http_client_request
1265+
):
1266+
request_kwargs = capture_and_mock_http_client_request(
1267+
self.http_client, mock_feature_flags, 200
1268+
)
1269+
1270+
feature_flags_response = syncify(
1271+
self.user_management.list_feature_flags(
1272+
user_id="user_01H7ZGXFP5C6BBQY6Z7277ZCT0"
1273+
)
1274+
)
1275+
1276+
def to_dict(x):
1277+
return x.dict()
1278+
1279+
assert request_kwargs["method"] == "get"
1280+
assert request_kwargs["url"].endswith(
1281+
"/user_management/users/user_01H7ZGXFP5C6BBQY6Z7277ZCT0/feature-flags"
1282+
)
1283+
assert (
1284+
list(map(to_dict, feature_flags_response.data))
1285+
== mock_feature_flags["data"]
1286+
)

tests/test_vault.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,34 @@ def test_read_object_none_object_id(self):
107107
):
108108
self.vault.read_object(object_id=None)
109109

110+
def test_read_object_by_name_success(
111+
self, mock_vault_object, capture_and_mock_http_client_request
112+
):
113+
request_kwargs = capture_and_mock_http_client_request(
114+
self.http_client, mock_vault_object, 200
115+
)
116+
117+
vault_object = self.vault.read_object_by_name(name="test-secret")
118+
119+
assert request_kwargs["method"] == "get"
120+
assert request_kwargs["url"].endswith("/vault/v1/kv/name/test-secret")
121+
assert vault_object.id == "vault_01234567890abcdef"
122+
assert vault_object.name == "test-secret"
123+
assert vault_object.value == "secret-value"
124+
assert vault_object.metadata.environment_id == "env_01234567890abcdef"
125+
126+
def test_read_object_by_name_missing_name(self):
127+
with pytest.raises(
128+
ValueError, match="Incomplete arguments: 'name' is a required argument"
129+
):
130+
self.vault.read_object_by_name(name="")
131+
132+
def test_read_object_by_name_none_name(self):
133+
with pytest.raises(
134+
ValueError, match="Incomplete arguments: 'name' is a required argument"
135+
):
136+
self.vault.read_object_by_name(name=None)
137+
110138
def test_list_objects_default_params(
111139
self, mock_vault_objects_list, capture_and_mock_http_client_request
112140
):

0 commit comments

Comments
 (0)