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
19 changes: 13 additions & 6 deletions sentry-api-client/Private/Invoke-SentryApiRequest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ function Invoke-SentryApiRequest {
)

$RequestParams = @{
Uri = $Uri
Method = $Method
Headers = $Script:SentryApiConfig.Headers
Uri = $Uri
Method = $Method
Headers = $Script:SentryApiConfig.Headers
ContentType = 'application/json'
}

Expand All @@ -25,10 +25,17 @@ function Invoke-SentryApiRequest {

try {
Write-Debug "Making $Method request to: $Uri"
$Response = Invoke-RestMethod @RequestParams

# Use Invoke-WebRequest instead of Invoke-RestMethod to get explicit control over JSON parsing
# Invoke-RestMethod silently returns strings when JSON parsing fails (e.g., with empty string keys)
$WebResponse = Invoke-WebRequest @RequestParams

# Explicitly parse JSON with error handling
# Use -AsHashtable to gracefully handle JSON with empty string keys (common in Sentry API responses)
$Response = $WebResponse.Content | ConvertFrom-Json -AsHashtable

return $Response
}
catch {
} catch {
$ErrorMessage = "Sentry API request ($Method $Uri) failed: $($_.Exception.Message)"
if ($_.Exception.Response) {
$StatusCode = $_.Exception.Response.StatusCode
Expand Down
84 changes: 44 additions & 40 deletions sentry-api-client/Tests/SentryApiClient.Fixtures.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@ AfterAll {
Describe 'SentryApiClient Tests with Real API Response Fixtures' {
Context 'Event Operations with Realistic Data' {
BeforeAll {
# Mock Invoke-RestMethod to return fixture data
Mock -ModuleName SentryApiClient Invoke-RestMethod {
param($Uri, $Method, $Headers, $ContentType, $Body)
# Mock Invoke-WebRequest to return fixture data
Mock -ModuleName SentryApiClient Invoke-WebRequest {
param($Uri)

switch -Regex ($Uri) {
'/events/4f1a9f7e7f7b4f9a8c8d9e0f1a2b3c4d/' {
return $script:Fixtures.event_detail
return @{ Content = ($script:Fixtures.event_detail | ConvertTo-Json -Depth 20) }
}
'/events/.*query=' {
return $script:Fixtures.event_list
return @{ Content = ($script:Fixtures.event_list | ConvertTo-Json -Depth 20) }
}
default {
throw [System.Net.WebException]::new("404 Not Found")
throw [System.Net.WebException]::new('404 Not Found')
}
}
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'my-org' -Project 'my-app'
}

Expand Down Expand Up @@ -90,36 +90,37 @@ Describe 'SentryApiClient Tests with Real API Response Fixtures' {

Context 'Issue Operations with Realistic Data' {
BeforeAll {
Mock -ModuleName SentryApiClient Invoke-RestMethod {
Mock -ModuleName SentryApiClient Invoke-WebRequest {
param($Uri)

switch -Regex ($Uri) {
'/events/4f1a9f7e7f7b4f9a8c8d9e0f1a2b3c4d/' {
# Handle Get-SentryEvent calls - most specific first
return $script:Fixtures.event_detail
return @{ Content = ($script:Fixtures.event_detail | ConvertTo-Json -Depth 20) }
}
'/issues/[^/]+/events/' {
# Handle issues/{id}/events/ endpoint - return event summaries
return @(
$responseData = @(
@{
eventID = '4f1a9f7e7f7b4f9a8c8d9e0f1a2b3c4d'
id = 'summary-id'
id = 'summary-id'
message = 'Event summary'
}
)
return @{ Content = ($responseData | ConvertTo-Json -Depth 20) }
}
'/issues/.*query=' {
return $script:Fixtures.issue_list
return @{ Content = ($script:Fixtures.issue_list | ConvertTo-Json -Depth 20) }
}
'/issues/1234567890/' {
return $script:Fixtures.issue_detail
return @{ Content = ($script:Fixtures.issue_detail | ConvertTo-Json -Depth 20) }
}
default {
throw [System.Net.WebException]::new("404 Not Found")
throw [System.Net.WebException]::new('404 Not Found')
}
}
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'my-org' -Project 'my-app'
}

Expand Down Expand Up @@ -149,68 +150,71 @@ Describe 'SentryApiClient Tests with Real API Response Fixtures' {

Context 'Error Response Handling' {
It 'Should handle 401 Unauthorized correctly' {
Mock -ModuleName SentryApiClient Invoke-RestMethod {
Mock -ModuleName SentryApiClient Invoke-WebRequest {
$response = New-Object System.Net.HttpWebResponse
$exception = [System.Net.WebException]::new(
"401 Unauthorized",
'401 Unauthorized',
$null,
[System.Net.WebExceptionStatus]::ProtocolError,
$response
)
throw $exception
}

Connect-SentryApi -ApiToken 'invalid-token' -Organization 'my-org' -Project 'my-app'
{ Get-SentryEvent -EventId 'test' } | Should -Throw "*401 Unauthorized*"

{ Get-SentryEvent -EventId 'test' } | Should -Throw '*401 Unauthorized*'
}

It 'Should handle 403 Forbidden correctly' {
Mock -ModuleName SentryApiClient Invoke-RestMethod {
Mock -ModuleName SentryApiClient Invoke-WebRequest {
$response = New-Object System.Net.HttpWebResponse
$exception = [System.Net.WebException]::new(
"403 Forbidden",
'403 Forbidden',
$null,
[System.Net.WebExceptionStatus]::ProtocolError,
$response
)
throw $exception
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'my-org' -Project 'my-app'
{ Get-SentryEvent -EventId 'forbidden-event' } | Should -Throw "*403 Forbidden*"

{ Get-SentryEvent -EventId 'forbidden-event' } | Should -Throw '*403 Forbidden*'
}

It 'Should handle 429 Rate Limit correctly' {
Mock -ModuleName SentryApiClient Invoke-RestMethod {
Mock -ModuleName SentryApiClient Invoke-WebRequest {
$response = New-Object System.Net.HttpWebResponse
$exception = [System.Net.WebException]::new(
"429 Too Many Requests",
'429 Too Many Requests',
$null,
[System.Net.WebExceptionStatus]::ProtocolError,
$response
)
throw $exception
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'my-org' -Project 'my-app'
{ Get-SentryEventsByTag -TagName 'test' -TagValue 'value' } | Should -Throw "*429 Too Many Requests*"

{ Get-SentryEventsByTag -TagName 'test' -TagValue 'value' } | Should -Throw '*429 Too Many Requests*'
}
}

Context 'Pagination Handling' {
BeforeAll {
Mock -ModuleName SentryApiClient Invoke-RestMethod {
param($Uri, $Method, $Headers)

# Return headers with Link header for pagination
$Headers['Link'] = '<https://sentry.io/api/0/projects/my-org/my-app/events/?&cursor=1234:100:0>; rel="next"; results="true"; cursor="1234:100:0"'

return $script:Fixtures.event_list
Mock -ModuleName SentryApiClient Invoke-WebRequest {
param($Uri)

# Return response with headers and content
return @{
Headers = @{
Link = '<https://sentry.io/api/0/projects/my-org/my-app/events/?&cursor=1234:100:0>; rel="next"; results="true"; cursor="1234:100:0"'
}
Content = ($script:Fixtures.event_list | ConvertTo-Json -Depth 20)
}
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'my-org' -Project 'my-app'
}

Expand Down
96 changes: 54 additions & 42 deletions sentry-api-client/Tests/SentryApiClient.Integration.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,14 @@ Describe 'SentryApiClient Integration Tests' {
Events = $testEvents
Issues = $testIssues
}

Mock -ModuleName SentryApiClient Invoke-RestMethod $mockResponder


# Wrap mock responder to return Invoke-WebRequest format
Mock -ModuleName SentryApiClient Invoke-WebRequest {
param($Uri)
$result = & $mockResponder -Uri $Uri
return @{ Content = ($result | ConvertTo-Json -Depth 20) }
}

# Setup connection
Connect-SentryApi -ApiToken 'test-token' -Organization 'test-org' -Project 'test-project'
}
Expand Down Expand Up @@ -74,34 +79,33 @@ Describe 'SentryApiClient Integration Tests' {

It 'Should find issues by tag and retrieve associated events' {
# Create a new mock that handles both issues and events
Mock -ModuleName SentryApiClient Invoke-RestMethod {
Mock -ModuleName SentryApiClient Invoke-WebRequest {
param($Uri)
if ($Uri -match '/events/[^/]+/') {

$result = if ($Uri -match '/events/[^/]+/') {
# Handle Get-SentryEvent calls - most specific first
return $testEvents | Where-Object { $_.id -eq 'event001' }
}
elseif ($Uri -match '/issues/[^/]+/events/') {
$testEvents | Where-Object { $_.id -eq 'event001' }
} elseif ($Uri -match '/issues/[^/]+/events/') {
# Return event summaries for the issue
return @(
@(
@{
eventID = 'event001'
id = 'summary-id'
id = 'summary-id'
message = 'Event summary'
}
)
}
elseif ($Uri -match '/issues/.*query=') {
} elseif ($Uri -match '/issues/.*query=') {
# Return issues that match the tag
return $testIssues | Where-Object { $_.title -match 'Database' }
}
else {
throw "Unexpected URI: $Uri"
$testIssues | Where-Object { $_.title -match 'Database' }
} else {
throw 'Unexpected URI: $Uri'
}

return @{ Content = ($result | ConvertTo-Json -Depth 20) }
}

$result = Find-SentryEventByTag -TagName 'severity' -TagValue 'high'

$result | Should -Not -BeNullOrEmpty
# Function now returns events array directly
if ($result.Count -gt 0) {
Expand All @@ -119,22 +123,23 @@ Describe 'SentryApiClient Integration Tests' {
)
}

Mock -ModuleName SentryApiClient Invoke-RestMethod {
Mock -ModuleName SentryApiClient Invoke-WebRequest {
param($Uri)

$queryParams = @{}
if ($Uri -match '\?(.+)$') {
$Matches[1] -split '&' | ForEach-Object {
$key, $value = $_ -split '=', 2
$queryParams[$key] = [System.Web.HttpUtility]::UrlDecode($value)
}
}

$limit = if ($queryParams['limit']) { [int]$queryParams['limit'] } else { 100 }

return $largeEventSet | Select-Object -First $limit

$result = $largeEventSet | Select-Object -First $limit
return @{ Content = ($result | ConvertTo-Json -Depth 20) }
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'test-org' -Project 'test-project'
}

Expand Down Expand Up @@ -164,8 +169,14 @@ Describe 'SentryApiClient Integration Tests' {
}

$mockResponder = New-MockSentryApiResponder -TestData $testData -SimulateRateLimit -RateLimitAfterCalls 2
Mock -ModuleName SentryApiClient Invoke-RestMethod $mockResponder


# Wrap mock responder to return Invoke-WebRequest format
Mock -ModuleName SentryApiClient Invoke-WebRequest {
param($Uri)
$result = & $mockResponder -Uri $Uri
return @{ Content = ($result | ConvertTo-Json -Depth 20) }
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'test-org' -Project 'test-project'

# First two calls should succeed
Expand All @@ -177,15 +188,14 @@ Describe 'SentryApiClient Integration Tests' {
}

It 'Should provide meaningful error for malformed responses' {
Mock -ModuleName SentryApiClient Invoke-RestMethod {
return "Not a valid JSON response"
Mock -ModuleName SentryApiClient Invoke-WebRequest {
return @{ Content = 'Not a valid JSON response' }
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'test-org' -Project 'test-project'

# The function should handle the malformed response gracefully
$result = Get-SentryEvent -EventId 'test'
$result | Should -Be "Not a valid JSON response"

# The function should now throw an error for malformed JSON
{ Get-SentryEvent -EventId 'test' } | Should -Throw
}
}

Expand All @@ -212,22 +222,24 @@ Describe 'SentryApiClient Integration Tests' {
)
)

Mock -ModuleName SentryApiClient Invoke-RestMethod {
Mock -ModuleName SentryApiClient Invoke-WebRequest {
param($Uri)

# Simple tag filtering logic for testing
if ($Uri -match 'query=(\w+)%3A(\w+)') {
$result = if ($Uri -match 'query=(\w+)%3A(\w+)') {
$tagName = $Matches[1]
$tagValue = $Matches[2]
return $complexEvents | Where-Object {

$complexEvents | Where-Object {
$_.tags | Where-Object { $_.key -eq $tagName -and $_.value -eq $tagValue }
}
} else {
$complexEvents
}
return $complexEvents

return @{ Content = ($result | ConvertTo-Json -Depth 20) }
}

Connect-SentryApi -ApiToken 'test-token' -Organization 'test-org' -Project 'test-project'
}

Expand Down
Loading