|
| 1 | +import time |
| 2 | +from unittest.mock import patch |
| 3 | + |
| 4 | +import jwt as pyjwt |
| 5 | +import pytest |
| 6 | + |
| 7 | +from sentry.conduit.auth import generate_channel_id, generate_conduit_token, get_conduit_credentials |
| 8 | +from tests.sentry.utils.test_jwt import RS256_KEY, RS256_PUB_KEY |
| 9 | + |
| 10 | + |
| 11 | +def test_generate_channel_id_is_valid_uuid(): |
| 12 | + """Should generate a valid uuid.""" |
| 13 | + channel_id = generate_channel_id() |
| 14 | + |
| 15 | + # UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
| 16 | + assert isinstance(channel_id, str) |
| 17 | + assert len(channel_id) == 36 # Length of UUID |
| 18 | + assert channel_id.count("-") == 4 |
| 19 | + |
| 20 | + |
| 21 | +def test_generate_channel_id_is_unique(): |
| 22 | + """Should generate unique channel_ids.""" |
| 23 | + assert generate_channel_id() != generate_channel_id() |
| 24 | + |
| 25 | + |
| 26 | +def test_generate_conduit_token_is_valid_jwt(): |
| 27 | + """Should generate a valid JWT token with RS256.""" |
| 28 | + org_id = 123 |
| 29 | + channel_id = "ad342057-d66b-4ed4-ab01-3415dd2cb1ce" |
| 30 | + |
| 31 | + token = generate_conduit_token( |
| 32 | + org_id, |
| 33 | + channel_id, |
| 34 | + issuer="sentry", |
| 35 | + audience="conduit", |
| 36 | + conduit_private_key=RS256_KEY, |
| 37 | + ) |
| 38 | + |
| 39 | + assert isinstance(token, str) |
| 40 | + assert token.count(".") == 2 |
| 41 | + |
| 42 | + claims = pyjwt.decode(token, RS256_PUB_KEY, algorithms=["RS256"], audience="conduit") |
| 43 | + |
| 44 | + assert claims["channel_id"] == channel_id |
| 45 | + assert claims["org_id"] == org_id |
| 46 | + assert claims["iss"] == "sentry" |
| 47 | + assert claims["aud"] == "conduit" |
| 48 | + assert "iat" in claims |
| 49 | + assert "exp" in claims |
| 50 | + |
| 51 | + |
| 52 | +def test_generate_conduit_token_has_expiration(): |
| 53 | + """Token should expire in 10 minutes.""" |
| 54 | + org_id = 123 |
| 55 | + channel_id = "ad342057-d66b-4ed4-ab01-3415dd2cb1ce" |
| 56 | + |
| 57 | + before_time = int(time.time()) |
| 58 | + token = generate_conduit_token( |
| 59 | + org_id, |
| 60 | + channel_id, |
| 61 | + issuer="sentry", |
| 62 | + audience="conduit", |
| 63 | + conduit_private_key=RS256_KEY, |
| 64 | + ) |
| 65 | + after_time = int(time.time()) |
| 66 | + |
| 67 | + claims = pyjwt.decode( |
| 68 | + token, |
| 69 | + RS256_PUB_KEY, |
| 70 | + algorithms=["RS256"], |
| 71 | + audience="conduit", |
| 72 | + options={"verify_exp": False}, |
| 73 | + ) |
| 74 | + |
| 75 | + exp_time = claims["exp"] |
| 76 | + iat_time = claims["iat"] |
| 77 | + |
| 78 | + assert iat_time >= before_time |
| 79 | + assert iat_time <= after_time |
| 80 | + assert exp_time == iat_time + 600 |
| 81 | + |
| 82 | + |
| 83 | +def test_generate_conduit_token_uses_settings(): |
| 84 | + """Should use settings when parameters are not provided.""" |
| 85 | + org_id = 123 |
| 86 | + channel_id = "ad342057-d66b-4ed4-ab01-3415dd2cb1ce" |
| 87 | + |
| 88 | + with patch("sentry.conduit.auth.settings") as mock_settings: |
| 89 | + mock_settings.CONDUIT_PRIVATE_KEY = RS256_KEY |
| 90 | + mock_settings.CONDUIT_JWT_ISSUER = "test-issuer" |
| 91 | + mock_settings.CONDUIT_JWT_AUDIENCE = "test-audience" |
| 92 | + |
| 93 | + token = generate_conduit_token( |
| 94 | + org_id, |
| 95 | + channel_id, |
| 96 | + ) |
| 97 | + |
| 98 | + claims = pyjwt.decode( |
| 99 | + token, |
| 100 | + RS256_PUB_KEY, |
| 101 | + algorithms=["RS256"], |
| 102 | + audience="test-audience", |
| 103 | + options={"verify_exp": False}, |
| 104 | + ) |
| 105 | + |
| 106 | + assert claims["iss"] == "test-issuer" |
| 107 | + assert claims["aud"] == "test-audience" |
| 108 | + |
| 109 | + |
| 110 | +def test_generate_conduit_token_raises_when_missing(): |
| 111 | + """Should raise an error if the private key is not configured.""" |
| 112 | + org_id = 123 |
| 113 | + channel_id = "ad342057-d66b-4ed4-ab01-3415dd2cb1ce" |
| 114 | + with pytest.raises(ValueError, match="CONDUIT_PRIVATE_KEY not configured"): |
| 115 | + generate_conduit_token( |
| 116 | + org_id, |
| 117 | + channel_id, |
| 118 | + ) |
| 119 | + |
| 120 | + |
| 121 | +def test_get_conduit_credentials_returns_all_credentials(): |
| 122 | + """Should return a url, token, and channel_id.""" |
| 123 | + gateway_url = "https://conduit.example.com" |
| 124 | + with patch("sentry.conduit.auth.settings") as mock_settings: |
| 125 | + mock_settings.CONDUIT_PRIVATE_KEY = RS256_KEY |
| 126 | + mock_settings.CONDUIT_JWT_ISSUER = "sentry" |
| 127 | + mock_settings.CONDUIT_JWT_AUDIENCE = "conduit" |
| 128 | + mock_settings.CONDUIT_GATEWAY_URL = gateway_url |
| 129 | + |
| 130 | + org_id = 123 |
| 131 | + result = get_conduit_credentials(org_id) |
| 132 | + |
| 133 | + assert isinstance(result.token, str) |
| 134 | + assert isinstance(result.channel_id, str) |
| 135 | + assert isinstance(result.url, str) |
| 136 | + |
| 137 | + assert str(org_id) in result.url |
| 138 | + assert result.url == f"{gateway_url}/events/{org_id}" |
| 139 | + |
| 140 | + |
| 141 | +def test_get_conduit_credentials_uses_custom_url(): |
| 142 | + """Should use provided gateway_url instead of settings.""" |
| 143 | + gateway_url = "https://custom.conduit.io" |
| 144 | + with patch("sentry.conduit.auth.settings") as mock_settings: |
| 145 | + mock_settings.CONDUIT_PRIVATE_KEY = RS256_KEY |
| 146 | + mock_settings.CONDUIT_JWT_ISSUER = "sentry" |
| 147 | + mock_settings.CONDUIT_JWT_AUDIENCE = "conduit" |
| 148 | + |
| 149 | + org_id = 123 |
| 150 | + result = get_conduit_credentials(org_id, gateway_url) |
| 151 | + |
| 152 | + assert isinstance(result.token, str) |
| 153 | + assert isinstance(result.channel_id, str) |
| 154 | + assert isinstance(result.url, str) |
| 155 | + |
| 156 | + assert str(org_id) in result.url |
| 157 | + assert result.url == f"{gateway_url}/events/{org_id}" |
| 158 | + |
| 159 | + |
| 160 | +def test_get_conduit_credentials_token_is_valid(): |
| 161 | + """Generated token should be decodable with correct claims.""" |
| 162 | + gateway_url = "https://conduit.example.com" |
| 163 | + with patch("sentry.conduit.auth.settings") as mock_settings: |
| 164 | + mock_settings.CONDUIT_PRIVATE_KEY = RS256_KEY |
| 165 | + mock_settings.CONDUIT_JWT_ISSUER = "sentry" |
| 166 | + mock_settings.CONDUIT_JWT_AUDIENCE = "conduit" |
| 167 | + mock_settings.CONDUIT_GATEWAY_URL = gateway_url |
| 168 | + |
| 169 | + org_id = 123 |
| 170 | + result = get_conduit_credentials(org_id) |
| 171 | + |
| 172 | + claims = pyjwt.decode( |
| 173 | + result.token, |
| 174 | + RS256_PUB_KEY, |
| 175 | + algorithms=["RS256"], |
| 176 | + audience="conduit", |
| 177 | + options={"verify_exp": False}, |
| 178 | + ) |
| 179 | + |
| 180 | + assert claims["org_id"] == org_id |
| 181 | + assert claims["channel_id"] == result.channel_id |
| 182 | + |
| 183 | + assert str(org_id) in result.url |
| 184 | + assert result.url == f"{gateway_url}/events/{org_id}" |
0 commit comments