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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

### Added

- New `Update-PatServerToken` public command for refreshing an expired or invalid Plex authentication token in a single step.
- Interactive PIN authentication via `plex.tv/link` (default), or supply `-Token` directly for automation
- Stores the new token via `Microsoft.PowerShell.SecretManagement` vault when available, otherwise inline in `servers.json`
- Verifies the new token against the Plex API root endpoint and reports the outcome
- Supports `-WhatIf` and `-Confirm`
- CI integration test workflow validates the configured `PLEX_TOKEN` secret in a dedicated pre-flight step. Expired or invalid tokens fail fast with an actionable `::error::` message instead of producing many cryptic test failures.

### Changed

- `401 Unauthorized` errors from the Plex API now include actionable token recovery guidance, naming `Update-PatServerToken`, `Get-PatStoredServer`, and the `-Token` parameter as concrete next steps. Detection lives centrally in the API wrapper, so every public cmdlet that hits the Plex API benefits. The original error message is appended to preserve URL/inner-exception detail for diagnostics.

## [0.10.3] - 2026-01-11

### Added
Expand Down
17 changes: 16 additions & 1 deletion PlexAutomationToolkit/Private/Invoke-PatApi.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,22 @@ function Invoke-PatApi {

if (-not $isTransient -or $attempt -eq $MaxRetries) {
# Non-transient error or final attempt - throw immediately
throw "Error invoking Plex API: $($_.Exception.Message)"
$errorMessage = $_.Exception.Message

# 401 indicates a missing, expired, or invalid token. Surface
# actionable guidance instead of the raw HTTP error so callers
# know which cmdlets to run to recover. Match \b401\b only —
# bare "Unauthorized" is too broad (UnauthorizedAccessException,
# 403 bodies mentioning the word, etc. would misfire).
if ($errorMessage -match '\b401\b') {
throw ("Plex API returned 401 Unauthorized. The authentication token is missing, expired, or invalid. " +
"To resolve: refresh the token with 'Update-PatServerToken' (use -Name to target a non-default server), " +
"list configured servers with 'Get-PatStoredServer', " +
"or pass an explicit -Token parameter to the cmdlet you are calling. " +
"Original error: $errorMessage")
}

Comment thread
tablackburn marked this conversation as resolved.
throw "Error invoking Plex API: $errorMessage"
}

# Calculate exponential backoff delay
Expand Down
36 changes: 29 additions & 7 deletions tests/Unit/Private/Invoke-PatApi.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,38 @@ Describe 'Invoke-PatApi' {
{ Invoke-PatApi -Uri 'http://localhost:32400/test' } | Should -Throw "*Error invoking Plex API*"
}

It 'Should propagate authentication errors' {
It 'Should surface actionable guidance for 401 Unauthorized errors' {
Mock Invoke-RestMethod {
$response = [PSCustomObject]@{
StatusCode = [System.Net.HttpStatusCode]::Unauthorized
}
$exception = [System.Net.WebException]::new('Unauthorized', $null, [System.Net.WebExceptionStatus]::ProtocolError, $response)
throw $exception
throw [System.Net.WebException]::new('The remote server returned an error: (401) Unauthorized.')
}

{ Invoke-PatApi -Uri 'http://localhost:32400/test' } | Should -Throw "*Error invoking Plex API*"
{ Invoke-PatApi -Uri 'http://localhost:32400/test' } | Should -Throw "*401 Unauthorized*Update-PatServerToken*Get-PatStoredServer*"
}

It 'Should include token recovery guidance when 401 is returned' {
Mock Invoke-RestMethod {
throw 'Response status code does not indicate success: 401 (Unauthorized).'
}

{ Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 1 -BaseDelaySeconds 0 } |
Should -Throw "*token is missing, expired, or invalid*Update-PatServerToken*Get-PatStoredServer*"
}

It 'Should preserve the original error message in the 401 guidance' {
$originalError = 'Response status code does not indicate success: 401 (Unauthorized).'
Mock Invoke-RestMethod { throw $originalError }

{ Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 1 -BaseDelaySeconds 0 } |
Should -Throw "*Original error: $originalError*"
}

It 'Should not trigger 401 guidance for non-401 errors that mention Unauthorized' {
Mock Invoke-RestMethod {
throw 'UnauthorizedAccessException: cannot read configuration file'
}

{ Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 1 -BaseDelaySeconds 0 } |
Should -Throw "*Error invoking Plex API*UnauthorizedAccessException*"
}

It 'Should handle malformed JSON responses' {
Expand Down