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
54 changes: 27 additions & 27 deletions RTShell/RTShell.psd1
Original file line number Diff line number Diff line change
@@ -1,42 +1,42 @@
@{
# Script module or binary module file associated with this manifest
RootModule = 'RTShell.psm1'

# Version number of this module.
ModuleVersion = '0.1.3'
ModuleVersion = '0.1.4'

# ID used to uniquely identify this module
GUID = 'ec49fac6-1b1c-4776-bab1-f9466887fc28'

# Author of this module
Author = 'Josh Dearing'

# Company or vendor of this module
CompanyName = ''

# Copyright statement for this module
Copyright = 'Joshua Dearing Copyright (c) 2026 '

# Description of the functionality provided by this module
Description = 'PowerShell module for Request Tracker (RT) via REST API v2. Supports API token auth, config persistence, structured ticket search, write operations, and response templates.'

# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '5.1'

# Modules that must be imported into the global environment prior to importing this module
RequiredModules = @('Microsoft.PowerShell.SecretManagement')

# Assemblies that must be loaded prior to importing this module
# RequiredAssemblies = @('bin\RTShell.dll')

# Type files (.ps1xml) to be loaded when importing this module
# Expensive for import time, no more than one should be used.
# TypesToProcess = @('xml\RTShell.Types.ps1xml')

# Format files (.ps1xml) to be loaded when importing this module.
# Expensive for import time, no more than one should be used.
# FormatsToProcess = @('xml\RTShell.Format.ps1xml')

# Functions to export from this module
FunctionsToExport = @(
# Session
Expand Down Expand Up @@ -71,43 +71,43 @@
'Set-RTTemplate'
'Remove-RTTemplate'
)

# Cmdlets to export from this module
CmdletsToExport = ''

# Variables to export from this module
VariablesToExport = ''

# Aliases to export from this module
AliasesToExport = ''

# List of all files packaged with this module
FileList = @()

# Private data to pass to the module specified in ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell.
PrivateData = @{

#Support for PowerShellGet galleries.
PSData = @{

# Tags applied to this module. These help with module discovery in online galleries.
Tags = @('RT', 'RequestTracker', 'Ticketing', 'ITSM')

# A URL to the license for this module.
LicenseUri = 'https://github.com/DearingDev/RTShell/blob/main/LICENSE'

# A URL to the main website for this project.
ProjectUri = 'https://github.com/DearingDev/RTShell'

# A URL to an icon representing this module.
# IconUri = ''

# ReleaseNotes of this module
ReleaseNotes = @'
0.1.3 - SearchRTTicket will now return queue name instead of queue id
0.1.4 - Date now properly displays as UTC. Added option to specify token name.
'@

} # End of PSData hashtable

} # End of PrivateData hashtable
}
12 changes: 8 additions & 4 deletions RTShell/functions/Connect-RT.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,24 @@
throw "No saved configuration found. Run Save-RTConfiguration first."
}
$BaseUri = $config.BaseUri
$secretName = if ($config.TokenName) { $config.TokenName } else { 'RTShell_Token' }

try {
$TokenPlainText = Get-Secret -Name 'RTShell_Token' -AsPlainText -ErrorAction Stop
$TokenPlainText = Get-Secret -Name $secretName -AsPlainText -ErrorAction Stop
}
catch {
throw "Could not retrieve 'RTShell_Token' from SecretManagement. Make sure your vault is unlocked or run Save-RTConfiguration to save your token."
throw "Could not retrieve '$secretName' from SecretManagement. Make sure your vault is unlocked or run Save-RTConfiguration to save your token."
}
}
elseif ($PSCmdlet.ParameterSetName -eq 'FromConfig' -and $BaseUri) {
$config = Get-RTConfig
$secretName = if ($config -and $config.TokenName) { $config.TokenName } else { 'RTShell_Token' }

try {
$TokenPlainText = Get-Secret -Name 'RTShell_Token' -AsPlainText -ErrorAction Stop
$TokenPlainText = Get-Secret -Name $secretName -AsPlainText -ErrorAction Stop
}
catch {
throw "No token provided and could not retrieve 'RTShell_Token' from SecretManagement."
throw "No token provided and could not retrieve '$secretName' from SecretManagement."
}
}
elseif ($PSCmdlet.ParameterSetName -eq 'SecureToken') {
Expand Down
2 changes: 1 addition & 1 deletion RTShell/functions/Get-RTTicket.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
param($val)
if (-not $val) { return $null }
try {
$dt = [datetime]$val
$dt = [datetime]::SpecifyKind([datetime]$val, [System.DateTimeKind]::Utc)
if ($dt.Year -le 1970) { return $null }
return $dt
}
Expand Down
24 changes: 17 additions & 7 deletions RTShell/functions/Save-RTConfiguration.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
.DESCRIPTION
Saves connection details to disk so Connect-RT can be called without
parameters. Config is stored in:
~/.rtshell/config.json -- BaseUri and queue cache (no secrets)
~/.rtshell/config.json -- BaseUri, token name, and queue cache (no secrets)

Once saved, Connect-RT can be called with no parameters:
Connect-RT
Expand All @@ -24,16 +24,21 @@
.PARAMETER TokenPlainText
API token as plain text. Useful for scripting/CI environments.

.PARAMETER TokenName
The name under which the API token is stored in the SecretManagement vault.
Defaults to 'RTShell_Token'. Change this if you prefer a different name or
manage multiple RT instances.

.EXAMPLE
$tok = Read-Host -AsSecureString -Prompt 'RT API Token'
Save-RTConfiguration -BaseUri 'https://rt.example.com' -Token $tok

Save configuration using a secure string token.

.EXAMPLE
Save-RTConfiguration -BaseUri 'https://rt.example.com' -TokenPlainText $env:RT_TOKEN
Save-RTConfiguration -BaseUri 'https://rt.example.com' -TokenPlainText $env:RT_TOKEN -TokenName 'MyRT_Token'

Save configuration using a plain text token from an environment variable.
Save configuration using a plain text token with a custom vault secret name.

.OUTPUTS
None.
Expand All @@ -50,7 +55,11 @@

[Parameter(Mandatory, ParameterSetName = 'PlainToken')]
[ValidateNotNullOrEmpty()]
[string]$TokenPlainText
[string]$TokenPlainText,

[Parameter()]
[ValidateNotNullOrEmpty()]
[string]$TokenName = 'RTShell_Token'
)

$BaseUri = $BaseUri.TrimEnd('/')
Expand All @@ -65,6 +74,7 @@
$existing = Get-RTConfig
$config = @{
BaseUri = $BaseUri
TokenName = $TokenName
QueueCache = if ($existing.QueueCache) { $existing.QueueCache } else { @() }
QueueCacheDate = if ($existing.QueueCacheDate) { $existing.QueueCacheDate } else { $null }
}
Expand All @@ -76,9 +86,9 @@
Initialize-RTSecretVault

# Save the token to SecretManagement
Set-Secret -Name 'RTShell_Token' -Secret $Token -NoClobber:$false
Set-Secret -Name $TokenName -Secret $Token -NoClobber:$false

Write-Information "Configuration saved." -InformationAction Continue
Write-Information " BaseUri : $BaseUri (saved to ~/.rtshell/config.json)" -InformationAction Continue
Write-Information " Token : saved to SecretManagement vault as 'RTShell_Token'" -InformationAction Continue
Write-Information " BaseUri : $BaseUri (saved to ~/.rtshell/config.json)" -InformationAction Continue
Write-Information " Token : saved to SecretManagement vault as '$TokenName'" -InformationAction Continue
}
4 changes: 2 additions & 2 deletions RTShell/functions/Search-RTTicket.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,8 @@
Owner = if ($item.Owner.id) { $item.Owner.id }
elseif ($item.Owner -is [string]) { $item.Owner }
else { $null }
Created = if ($item.Created) { try { [datetime]$item.Created } catch { $null } } else { $null }
LastUpdated = if ($item.LastUpdated) { try { [datetime]$item.LastUpdated } catch { $null } } else { $null }
Created = if ($item.Created) { try { [datetime]::SpecifyKind([datetime]$item.Created, [System.DateTimeKind]::Utc) } catch { $null } } else { $null }
LastUpdated = if ($item.LastUpdated) { try { [datetime]::SpecifyKind([datetime]$item.LastUpdated, [System.DateTimeKind]::Utc) } catch { $null } } else { $null }
}
$obj
# Add numerical_id alias so Get-RTTicket can consume via pipeline
Expand Down
79 changes: 79 additions & 0 deletions Tests/functions/Get-RTTicket.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#Requires -Module Pester

BeforeAll {
$modulePath = Join-Path $PSScriptRoot '..\..\RTShell\RTShell.psd1'
Import-Module $modulePath -Force -ErrorAction Stop
}

Describe 'Get-RTTicket — datetime UTC kind' {

BeforeAll {
InModuleScope RTShell {
$script:fakeRaw = [PSCustomObject]@{
id = 42
Subject = 'Test ticket'
Status = 'open'
Queue = [PSCustomObject]@{ id = ''; _url = '' }
Owner = 'Nobody'
Requestor = @()
Cc = @()
AdminCc = @()
Priority = 0
FinalPriority = 0
InitialPriority = 0
TimeEstimated = 0
TimeWorked = 0
TimeLeft = 0
Created = '2026-04-24 21:38:00'
Starts = '2026-04-24 00:00:00'
Started = '2026-04-24 00:00:00'
Due = '2026-04-30 00:00:00'
Resolved = '1970-01-01 00:00:00'
LastUpdated = '2026-04-24 21:40:00'
CustomFields = @()
}

Mock Invoke-RTRequest { $script:fakeRaw } -ParameterFilter { $Path -like 'ticket/*' }
}

$script:Ticket = Get-RTTicket -Id 42
}

It 'Created has DateTimeKind.Utc' {
$script:Ticket.Created.Kind | Should -Be ([System.DateTimeKind]::Utc)
}

It 'LastUpdated has DateTimeKind.Utc' {
$script:Ticket.LastUpdated.Kind | Should -Be ([System.DateTimeKind]::Utc)
}

It 'Resolved is null for epoch sentinel 1970-01-01' {
$script:Ticket.Resolved | Should -BeNullOrEmpty
}

Context '-Detailed' {
BeforeAll {
$script:Detailed = Get-RTTicket -Id 42 -Detailed
}

It 'Created has DateTimeKind.Utc' {
$script:Detailed.Created.Kind | Should -Be ([System.DateTimeKind]::Utc)
}

It 'LastUpdated has DateTimeKind.Utc' {
$script:Detailed.LastUpdated.Kind | Should -Be ([System.DateTimeKind]::Utc)
}

It 'Starts has DateTimeKind.Utc' {
$script:Detailed.Starts.Kind | Should -Be ([System.DateTimeKind]::Utc)
}

It 'Started has DateTimeKind.Utc' {
$script:Detailed.Started.Kind | Should -Be ([System.DateTimeKind]::Utc)
}

It 'Due has DateTimeKind.Utc' {
$script:Detailed.Due.Kind | Should -Be ([System.DateTimeKind]::Utc)
}
}
}
41 changes: 41 additions & 0 deletions Tests/functions/Search-RTTicket.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#Requires -Module Pester

BeforeAll {
$modulePath = Join-Path $PSScriptRoot '..\..\RTShell\RTShell.psd1'
Import-Module $modulePath -Force -ErrorAction Stop
}

Describe 'Search-RTTicket — datetime UTC kind' {

BeforeAll {
InModuleScope RTShell {
$script:fakeResponse = [PSCustomObject]@{
total = 1
page = 1
items = @(
[PSCustomObject]@{
id = 99
Subject = 'Test search ticket'
Status = 'open'
Queue = [PSCustomObject]@{ Name = 'General'; id = '' }
Owner = [PSCustomObject]@{ id = 'Nobody' }
Created = '2026-04-24 21:38:00'
LastUpdated = '2026-04-24 21:40:00'
}
)
}

Mock Invoke-RTRequest { $script:fakeResponse } -ParameterFilter { $Path -eq 'tickets' }
}

$script:Result = Search-RTTicket -Query 'id=99' | Select-Object -First 1
}

It 'Created has DateTimeKind.Utc' {
$script:Result.Created.Kind | Should -Be ([System.DateTimeKind]::Utc)
}

It 'LastUpdated has DateTimeKind.Utc' {
$script:Result.LastUpdated.Kind | Should -Be ([System.DateTimeKind]::Utc)
}
}
7 changes: 6 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@

## 0.1.3 (2026-04-08)

+ Search-RTTicket now returns QueueName
+ Search-RTTicket now returns QueueName

## 0.1.4 (2026-04-25)

+ Dates now properly return as UTC.
+ Added option to select a different token name and store it in the config.json