Skip to content

Commit b030245

Browse files
gjtorikianclaude
andcommitted
Add vault describe_object endpoint and tests for passwordless/vault
- Add describe_object (sync + async) to vault.py for GET /vault/v1/kv/:id/metadata, matching the Node SDK's describeObject method - Add test_passwordless.py with sync + async tests for create_session and send_session - Add test_vault.py with sync + async tests for all vault operations including read, describe, list, create, update, delete, data keys, and encrypt/decrypt round-trip - Add JSON fixtures for passwordless and vault test data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7688f27 commit b030245

11 files changed

+698
-0
lines changed

src/workos/vault.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,15 @@ def read_object_by_name(self, *, name: str) -> VaultObject:
302302
)
303303
return response
304304

305+
def describe_object(self, *, object_id: str) -> VaultObject:
306+
"""Get a Vault object's metadata without decrypting the value."""
307+
response = self._client.request(
308+
method="get",
309+
path=f"vault/v1/kv/{object_id}/metadata",
310+
model=VaultObject,
311+
)
312+
return response
313+
305314
def list_objects(
306315
self,
307316
*,
@@ -475,6 +484,14 @@ async def read_object_by_name(self, *, name: str) -> VaultObject:
475484
)
476485
return response
477486

487+
async def describe_object(self, *, object_id: str) -> VaultObject:
488+
response = await self._client.request(
489+
method="get",
490+
path=f"vault/v1/kv/{object_id}/metadata",
491+
model=VaultObject,
492+
)
493+
return response
494+
478495
async def list_objects(
479496
self,
480497
*,
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"object": "passwordless_session",
3+
"id": "passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C",
4+
"email": "user@example.com",
5+
"expires_at": "2025-01-01T00:00:00.000Z",
6+
"link": "https://auth.workos.com/passwordless/01EHDAK2BFGWCSZXP9HGZ3VK8C/confirm"
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"context": {"tenant": "acme"},
3+
"environment_id": "environment_01EHDAK2BFGWCSZXP9HGZ3VK8C",
4+
"id": "vault_obj_01EHDAK2BFGWCSZXP9HGZ3VK8C",
5+
"key_id": "vault_key_01EHDAK2BFGWCSZXP9HGZ3VK8C",
6+
"updated_at": "2025-01-01T00:00:00.000Z",
7+
"updated_by": {
8+
"id": "key_01EHDAK2BFGWCSZXP9HGZ3VK8C",
9+
"name": "My API Key"
10+
},
11+
"version_id": "vault_ver_01EHDAK2BFGWCSZXP9HGZ3VK8C"
12+
}

tests/fixtures/vault_data_key.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"context": {"tenant": "acme"},
3+
"id": "vault_key_01EHDAK2BFGWCSZXP9HGZ3VK8C",
4+
"data_key": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=",
5+
"encrypted_keys": "dGVzdC1lbmNyeXB0ZWQta2V5cw=="
6+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"id": "vault_key_01EHDAK2BFGWCSZXP9HGZ3VK8C",
3+
"data_key": "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE="
4+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"data": [
3+
{
4+
"created_at": "2025-01-01T00:00:00.000Z",
5+
"current_version": true,
6+
"id": "vault_ver_01EHDAK2BFGWCSZXP9HGZ3VK8C"
7+
}
8+
]
9+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"data": [
3+
{
4+
"id": "vault_obj_01EHDAK2BFGWCSZXP9HGZ3VK8C",
5+
"name": "my-secret",
6+
"updated_at": "2025-01-01T00:00:00.000Z"
7+
}
8+
],
9+
"list_metadata": {
10+
"before": null,
11+
"after": null
12+
}
13+
}

tests/fixtures/vault_object.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"id": "vault_obj_01EHDAK2BFGWCSZXP9HGZ3VK8C",
3+
"metadata": {
4+
"context": {"tenant": "acme"},
5+
"environment_id": "environment_01EHDAK2BFGWCSZXP9HGZ3VK8C",
6+
"id": "vault_obj_01EHDAK2BFGWCSZXP9HGZ3VK8C",
7+
"key_id": "vault_key_01EHDAK2BFGWCSZXP9HGZ3VK8C",
8+
"updated_at": "2025-01-01T00:00:00.000Z",
9+
"updated_by": {
10+
"id": "key_01EHDAK2BFGWCSZXP9HGZ3VK8C",
11+
"name": "My API Key"
12+
},
13+
"version_id": "vault_ver_01EHDAK2BFGWCSZXP9HGZ3VK8C"
14+
},
15+
"name": "my-secret",
16+
"value": "super-secret-value"
17+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"id": "vault_obj_01EHDAK2BFGWCSZXP9HGZ3VK8C",
3+
"metadata": {
4+
"context": {"tenant": "acme"},
5+
"environment_id": "environment_01EHDAK2BFGWCSZXP9HGZ3VK8C",
6+
"id": "vault_obj_01EHDAK2BFGWCSZXP9HGZ3VK8C",
7+
"key_id": "vault_key_01EHDAK2BFGWCSZXP9HGZ3VK8C",
8+
"updated_at": "2025-01-01T00:00:00.000Z",
9+
"updated_by": {
10+
"id": "key_01EHDAK2BFGWCSZXP9HGZ3VK8C",
11+
"name": "My API Key"
12+
},
13+
"version_id": "vault_ver_01EHDAK2BFGWCSZXP9HGZ3VK8C"
14+
},
15+
"name": "my-secret"
16+
}

tests/test_passwordless.py

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import json
2+
3+
import pytest
4+
from workos import WorkOS, AsyncWorkOS
5+
from tests.generated_helpers import load_fixture
6+
7+
from workos.passwordless import PasswordlessSession
8+
from workos._errors import (
9+
AuthenticationException,
10+
NotFoundException,
11+
RateLimitExceededException,
12+
ServerException,
13+
)
14+
15+
16+
class TestPasswordless:
17+
def test_create_session(self, workos, httpx_mock):
18+
httpx_mock.add_response(
19+
json=load_fixture("passwordless_session.json"),
20+
)
21+
result = workos.passwordless.create_session(
22+
email="user@example.com", type="MagicLink"
23+
)
24+
assert isinstance(result, PasswordlessSession)
25+
assert result.id == "passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C"
26+
assert result.email == "user@example.com"
27+
assert result.link.startswith("https://auth.workos.com/passwordless/")
28+
request = httpx_mock.get_request()
29+
assert request.method == "POST"
30+
assert request.url.path.endswith("/passwordless/sessions")
31+
body = json.loads(request.content)
32+
assert body["email"] == "user@example.com"
33+
assert body["type"] == "MagicLink"
34+
35+
def test_create_session_with_optional_params(self, workos, httpx_mock):
36+
httpx_mock.add_response(
37+
json=load_fixture("passwordless_session.json"),
38+
)
39+
result = workos.passwordless.create_session(
40+
email="user@example.com",
41+
type="MagicLink",
42+
redirect_uri="https://example.com/callback",
43+
state="custom-state",
44+
expires_in=3600,
45+
)
46+
assert isinstance(result, PasswordlessSession)
47+
request = httpx_mock.get_request()
48+
body = json.loads(request.content)
49+
assert body["redirect_uri"] == "https://example.com/callback"
50+
assert body["state"] == "custom-state"
51+
assert body["expires_in"] == 3600
52+
53+
def test_send_session(self, workos, httpx_mock):
54+
httpx_mock.add_response(status_code=204)
55+
result = workos.passwordless.send_session(
56+
"passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C"
57+
)
58+
assert result is True
59+
request = httpx_mock.get_request()
60+
assert request.method == "POST"
61+
assert request.url.path.endswith(
62+
"/passwordless/sessions/passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C/send"
63+
)
64+
65+
def test_create_session_unauthorized(self, workos, httpx_mock):
66+
httpx_mock.add_response(
67+
status_code=401,
68+
json={"message": "Unauthorized"},
69+
)
70+
with pytest.raises(AuthenticationException):
71+
workos.passwordless.create_session(
72+
email="user@example.com", type="MagicLink"
73+
)
74+
75+
def test_create_session_not_found(self, httpx_mock):
76+
workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0)
77+
try:
78+
httpx_mock.add_response(status_code=404, json={"message": "Not found"})
79+
with pytest.raises(NotFoundException):
80+
workos.passwordless.create_session(
81+
email="user@example.com", type="MagicLink"
82+
)
83+
finally:
84+
workos.close()
85+
86+
def test_create_session_rate_limited(self, httpx_mock):
87+
workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0)
88+
try:
89+
httpx_mock.add_response(
90+
status_code=429,
91+
headers={"Retry-After": "0"},
92+
json={"message": "Slow down"},
93+
)
94+
with pytest.raises(RateLimitExceededException):
95+
workos.passwordless.create_session(
96+
email="user@example.com", type="MagicLink"
97+
)
98+
finally:
99+
workos.close()
100+
101+
def test_create_session_server_error(self, httpx_mock):
102+
workos = WorkOS(api_key="sk_test_123", client_id="client_test", max_retries=0)
103+
try:
104+
httpx_mock.add_response(status_code=500, json={"message": "Server error"})
105+
with pytest.raises(ServerException):
106+
workos.passwordless.create_session(
107+
email="user@example.com", type="MagicLink"
108+
)
109+
finally:
110+
workos.close()
111+
112+
113+
@pytest.mark.asyncio
114+
class TestAsyncPasswordless:
115+
async def test_create_session(self, async_workos, httpx_mock):
116+
httpx_mock.add_response(json=load_fixture("passwordless_session.json"))
117+
result = await async_workos.passwordless.create_session(
118+
email="user@example.com", type="MagicLink"
119+
)
120+
assert isinstance(result, PasswordlessSession)
121+
assert result.id == "passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C"
122+
assert result.email == "user@example.com"
123+
request = httpx_mock.get_request()
124+
assert request.method == "POST"
125+
assert request.url.path.endswith("/passwordless/sessions")
126+
127+
async def test_create_session_with_optional_params(self, async_workos, httpx_mock):
128+
httpx_mock.add_response(json=load_fixture("passwordless_session.json"))
129+
result = await async_workos.passwordless.create_session(
130+
email="user@example.com",
131+
type="MagicLink",
132+
redirect_uri="https://example.com/callback",
133+
state="custom-state",
134+
expires_in=3600,
135+
)
136+
assert isinstance(result, PasswordlessSession)
137+
request = httpx_mock.get_request()
138+
body = json.loads(request.content)
139+
assert body["redirect_uri"] == "https://example.com/callback"
140+
assert body["state"] == "custom-state"
141+
assert body["expires_in"] == 3600
142+
143+
async def test_send_session(self, async_workos, httpx_mock):
144+
httpx_mock.add_response(status_code=204)
145+
result = await async_workos.passwordless.send_session(
146+
"passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C"
147+
)
148+
assert result is True
149+
request = httpx_mock.get_request()
150+
assert request.method == "POST"
151+
assert request.url.path.endswith(
152+
"/passwordless/sessions/passwordless_session_01EHDAK2BFGWCSZXP9HGZ3VK8C/send"
153+
)
154+
155+
async def test_create_session_unauthorized(self, async_workos, httpx_mock):
156+
httpx_mock.add_response(status_code=401, json={"message": "Unauthorized"})
157+
with pytest.raises(AuthenticationException):
158+
await async_workos.passwordless.create_session(
159+
email="user@example.com", type="MagicLink"
160+
)
161+
162+
async def test_create_session_not_found(self, httpx_mock):
163+
workos = AsyncWorkOS(
164+
api_key="sk_test_123", client_id="client_test", max_retries=0
165+
)
166+
try:
167+
httpx_mock.add_response(status_code=404, json={"message": "Not found"})
168+
with pytest.raises(NotFoundException):
169+
await workos.passwordless.create_session(
170+
email="user@example.com", type="MagicLink"
171+
)
172+
finally:
173+
await workos.close()
174+
175+
async def test_create_session_rate_limited(self, httpx_mock):
176+
workos = AsyncWorkOS(
177+
api_key="sk_test_123", client_id="client_test", max_retries=0
178+
)
179+
try:
180+
httpx_mock.add_response(
181+
status_code=429,
182+
headers={"Retry-After": "0"},
183+
json={"message": "Slow down"},
184+
)
185+
with pytest.raises(RateLimitExceededException):
186+
await workos.passwordless.create_session(
187+
email="user@example.com", type="MagicLink"
188+
)
189+
finally:
190+
await workos.close()
191+
192+
async def test_create_session_server_error(self, httpx_mock):
193+
workos = AsyncWorkOS(
194+
api_key="sk_test_123", client_id="client_test", max_retries=0
195+
)
196+
try:
197+
httpx_mock.add_response(status_code=500, json={"message": "Server error"})
198+
with pytest.raises(ServerException):
199+
await workos.passwordless.create_session(
200+
email="user@example.com", type="MagicLink"
201+
)
202+
finally:
203+
await workos.close()

0 commit comments

Comments
 (0)