Skip to content

Initial implementation of go-remedy#1

Merged
tphakala merged 3 commits intomainfrom
initial-implementation
Dec 17, 2025
Merged

Initial implementation of go-remedy#1
tphakala merged 3 commits intomainfrom
initial-implementation

Conversation

@tphakala
Copy link
Owner

@tphakala tphakala commented Dec 17, 2025

Summary

  • Complete Go client library for BMC Remedy AR System REST API
  • JWT authentication with automatic token refresh (lazy evaluation, double-check locking)
  • Entry CRUD operations with type-safe query builder
  • Attachment upload/download support
  • Request serialization to prevent BMC Error 9093
  • Token bucket rate limiting
  • Zero external runtime dependencies

Test plan

  • All unit tests pass (go test ./...)
  • Linter passes (golangci-lint run)
  • Integration test against real Remedy server (manual)

Summary by CodeRabbit

  • New Features

    • Go client library for Remedy REST with JWT auth, automatic token refresh, and pluggable options.
    • CRUD and merge operations for form entries, attachment upload/download, and a type-safe query builder.
    • Configurable rate limiting and per-client request serialization.
  • Enhancements

    • Rich API error type and sentinel errors for common HTTP error cases.
    • Public interfaces and data types to ease testing and integration.
  • Documentation

    • README and MIT license added.
  • Chores

    • CI workflow, lint config, gitignore, and module declaration added.
  • Tests

    • Extensive unit tests across components.

✏️ Tip: You can customize this high-level summary in your review settings.

Features:
- JWT authentication with automatic token refresh
- Entry CRUD operations (Create, Read, Update, Delete, Merge)
- Attachment upload and download
- Type-safe query builder for AR qualifications
- Built-in request serialization (prevents BMC Error 9093)
- Token bucket rate limiting
- Context-aware with cancellation support
- Zero external runtime dependencies (stdlib only)

Token refresh uses lazy evaluation with double-check locking
for thread-safe credential management and automatic re-authentication.
@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

Caution

Review failed

The pull request is closed.

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a new go-remedy client library: typed models and interfaces, a thread-safe Client with JWT auth and auto-refresh, per-client request queue and optional rate limiter, entry CRUD and attachment streaming/upload, a query-builder, centralized API error types, CI/lint/configuration, and comprehensive unit tests.

Changes

Cohort / File(s) Summary
Repository config & CI
\.github/workflows/ci.yml, \.gitignore, \.golangci.yaml, LICENSE, go.mod
CI workflow (test/lint/build matrix), git ignore patterns, golangci-lint config, MIT license, module definition with test deps.
Documentation
README.md
Project README with overview, installation, quickstart, usage examples, testing notes, and license.
Public types & interfaces
types.go, interfaces.go, errors.go
Data models (Entry, EntryList, Link, Field, SortOrder, DeleteOption), APIError type + Error/Is, sentinel errors, and public interfaces (HTTPDoer, EntryServicer, AttachmentServicer, RemedyClient).
Client core & options
client.go, options.go
Client type, constructor New, Close, Entries/Attachments accessors, request queue/rate-limit integration, request lifecycle, and functional options (HTTP client, timeout, rate limit, token lifetime/refresh, auto-refresh).
Authentication & token management
auth.go, token_refresh_test.go
Login/LoginWithAuth, Logout, IsAuthenticated, token storage/expiry/refresh logic, ErrTokenTooLarge, and extensive token refresh tests.
Entry service
entry.go, entry_test.go
EntryService implementation: Get, List, Create, Update, Delete, Merge with validation, queueing, rate limiting, query param construction and tests.
Attachment service
attachment.go, attachment_test.go
AttachmentService implementation: Get (streaming/cancellable) and Upload (multipart streaming) and corresponding tests.
Query builder
query.go, query_test.go
Type-safe AR qualification builder with And/Or, safe operator validation, raw expressions, field escaping, value formatting, and tests.
Internal utilities
internal/queue/queue.go, internal/queue/queue_test.go, internal/ratelimit/ratelimit.go, internal/ratelimit/ratelimit_test.go
Per-client single-slot queue (Acquire/Release/Close) and token-bucket rate limiter (Allow/Wait) with comprehensive tests.
Test harness & client tests
client_test.go, attachment_test.go, entry_test.go, query_test.go, internal/..._test.go
Extensive unit tests covering client init, auth flows, API error parsing, query param building, CRUD, attachments, queue and rate limiter concurrency, context cancellation, and refresh scenarios.

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant Client
participant Queue
participant Limiter
participant HTTP as HTTP Server
Note over Client,HTTP: Outbound API request (entry/attachment) flow
Client->>Queue: Acquire(ctx)
alt acquire acquired
Queue-->>Client: token
Client->>Limiter: Wait(ctx) or Allow()
alt token available
Limiter-->>Client: proceed
else wait
Limiter-->>Client: waited -> proceed
end
Client->>HTTP: HTTP request (with Authorization header)
HTTP-->>Client: 2xx / error
Client->>Queue: Release()
else queue closed or ctx canceled
Queue-->>Client: ErrQueueClosed or ctx.Err()
end

mermaid
sequenceDiagram
autonumber
participant Caller
participant Client
participant AuthServer as Auth/API
Note over Client,AuthServer: Login / token refresh flow
Caller->>Client: Login(ctx, user, pass)
Client->>AuthServer: POST /login
AuthServer-->>Client: 200 {token, expires_in}
Client->>Client: store token, set expiry
Note right of Client: Later, request triggers NeedRefresh?
Caller->>Client: DoRequest(ctx...)
Client->>Client: check token expiry -> needs refresh
opt have credentials & auto-refresh enabled
Client->>AuthServer: POST /login (refresh)
AuthServer-->>Client: 200 {token, expires_in}
Client->>Client: replace token, continue request
else no credentials
Client-->>Caller: ErrNoCredentials / APIError
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–75 minutes

  • Areas needing extra attention:
    • client.go — request lifecycle, concurrent access to token/credentials, header injection, error parsing.
    • auth.go & token refresh tests — token expiry/refresh threshold, concurrency during refresh, ErrTokenTooLarge handling.
    • internal/queue/queue.go — Acquire/Release semantics, cancellation behavior, Close idempotency.
    • internal/ratelimit/ratelimit.go — refill math, Wait timing and context handling.
    • attachment.go — multipart streaming via pipe and goroutine error propagation (wrapUploadError).

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.73% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Initial implementation of go-remedy' clearly and specifically identifies the main change: introducing a new Go client library for the BMC Remedy AR System REST API.

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 77791c8 and 4d56e55.

📒 Files selected for processing (5)
  • README.md (1 hunks)
  • attachment.go (1 hunks)
  • auth.go (1 hunks)
  • internal/ratelimit/ratelimit.go (1 hunks)
  • internal/ratelimit/ratelimit_test.go (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@socket-security
Copy link

socket-security bot commented Dec 17, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedgithub.com/​stretchr/​testify@​v1.11.196100100100100

View full report

@gemini-code-assist
Copy link

Summary of Changes

Hello @tphakala, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers the initial implementation of go-remedy, a new Go client library designed to streamline interactions with the BMC Remedy AR System REST API. It provides a robust and feature-rich solution for managing Remedy data, focusing on secure authentication, efficient data operations, and resilience against common API challenges. The library aims to offer a complete and reliable toolkit for Go developers working with BMC Remedy.

Highlights

  • Complete Go Client Library: This pull request introduces a comprehensive Go client library for interacting with the BMC Remedy AR System REST API, providing a native and idiomatic Go experience.
  • Robust Authentication & Token Management: The library features JWT authentication with automatic token refresh, employing lazy evaluation and double-check locking for efficient and secure session management. Credentials are securely stored for seamless re-authentication.
  • Entry CRUD Operations with Type-Safe Query Builder: It supports full CRUD (Create, Read, Update, Delete, Merge) operations for Remedy entries, complemented by a type-safe query builder for constructing complex AR qualification strings, enhancing developer productivity and reducing errors.
  • Attachment Handling and Request Serialization: The client includes support for attachment upload and download. Crucially, it implements built-in request serialization to prevent BMC Error 9093, which occurs due to concurrent requests from the same user session.
  • Performance and Reliability Features: Token bucket rate limiting is integrated to prevent overwhelming the Remedy server, and the library is context-aware with cancellation support. It boasts zero external runtime dependencies, relying solely on the Go standard library.
Ignored Files
  • Ignored by pattern: .github/workflows/** (1)
    • .github/workflows/ci.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@tphakala
Copy link
Owner Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 17, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the initial implementation of go-remedy, a Go client library for the BMC Remedy AR System REST API. The implementation is comprehensive, covering authentication with auto-refresh, CRUD operations, attachments, rate limiting, and request serialization. The code is well-structured with good use of interfaces for testability and internal packages for implementation details. The test suite is extensive and covers many edge cases, especially for the complex token refresh and concurrency logic.

I've found a few issues, including a potential goroutine leak, an incorrect JSON tag, and a minor state inconsistency during logout. I've also suggested an improvement to an example in the README to promote better error handling practices. Overall, this is a very strong initial implementation.

Comment on lines +87 to +90
req, cancel, err := s.client.newRequest(ctx, http.MethodPost, path, pr)
if err != nil {
return fmt.Errorf("creating upload request: %w", err)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a potential goroutine leak in the Upload function. If s.client.newRequest returns an error, the function returns, but the goroutine started earlier to handle the multipart writing is not terminated. It may block forever on writing to the pipe pw, as nothing will be reading from the other end pr. To fix this, you should close the pipe writer with an error to unblock the goroutine.

Suggested change
req, cancel, err := s.client.newRequest(ctx, http.MethodPost, path, pr)
if err != nil {
return fmt.Errorf("creating upload request: %w", err)
}
req, cancel, err := s.client.newRequest(ctx, http.MethodPost, path, pr)
if err != nil {
_ = pw.CloseWithError(err) // Unblock the writer goroutine
return fmt.Errorf("creating upload request: %w", err)
}

README.md Outdated
Comment on lines +232 to +236
file, _ := os.Open("document.pdf")
defer file.Close()

err := client.Attachments().Upload(ctx, "Form", "EntryID", "FieldName",
"document.pdf", file)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The example code for uploading an attachment ignores the error returned by os.Open. This is not a good practice to demonstrate in documentation, as it can lead to a nil pointer panic if the file fails to open. Additionally, the error from the Upload call is declared but not handled in the snippet.

It would be better to show proper error handling for both operations, for example:

// Upload attachment
file, err := os.Open("document.pdf")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

if err := client.Attachments().Upload(ctx, "Form", "EntryID", "FieldName", "document.pdf", file); err != nil {
    log.Fatal(err)
}

auth.go Outdated
resp, err := c.do(req)
if err != nil {
// Clear token even if request fails
c.setToken("")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the Logout function, when clearing the token upon request failure, only the token string is cleared via c.setToken(""). The tokenExpiry time is left unchanged. For better state consistency, it's advisable to clear both the token and its expiry time.

Suggested change
c.setToken("")
c.setTokenWithExpiry("", time.Time{})

auth.go Outdated
}()

// Clear token regardless of response
c.setToken("")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the Logout function, after a successful logout, only the token string is cleared via c.setToken(""). The tokenExpiry time is left unchanged. For better state consistency, it's advisable to clear both the token and its expiry time.

Suggested change
c.setToken("")
c.setTokenWithExpiry("", time.Time{})

- attachment.go: Fix goroutine leak by closing pipe writer on error
- auth.go: Clear both token and expiry on logout for state consistency
- README.md: Add proper error handling to upload example
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
attachment.go (2)

64-83: Consider calling writer.Close() on all exit paths.

If io.Copy fails (line 77-80), the function returns without calling writer.Close(). While the error will be propagated and the pipe will be closed, explicitly closing the multipart writer ensures consistent cleanup. This is a minor improvement for defensive coding.

 go func() {
 	defer func() {
+		_ = writer.Close() // Ensure multipart writer is closed
 		_ = pw.Close() // Always close pipe
 	}()

 	part, err := writer.CreateFormFile("entry", filename)
 	if err != nil {
 		errCh <- fmt.Errorf("creating form file: %w", err)
 		return
 	}

 	if _, err := io.Copy(part, data); err != nil {
 		errCh <- fmt.Errorf("copying data: %w", err)
 		return
 	}

-	errCh <- writer.Close()
+	errCh <- nil
 }()

115-128: Non-blocking channel receive may miss write errors.

The select with default on line 124 means if the HTTP request fails before the goroutine finishes writing, the write error is silently dropped. While the HTTP error is still returned, this could hide the root cause (e.g., a read error from the input data). Consider documenting this behavior or using a short timeout instead.

token_refresh_test.go (1)

145-171: Test uses a 3-second sleep.

The time.Sleep(3 * time.Second) on line 167 makes this test slow. While acceptable for testing time-based behavior, consider if the lifetime/threshold values could be reduced further to speed up the test suite.

query.go (1)

71-80: Raw always uses AND conjunction.

The Raw method always joins with AND, which may be unexpected if users want OR logic with their raw qualification. Consider documenting this limitation more explicitly or adding a parameter to control the conjunction.

Example documentation addition:

-// Raw adds a raw qualification string with AND conjunction.
+// Raw adds a raw qualification string joined with AND.
+// If you need OR conjunction, start with Or() before calling Raw().
 // Use this for complex expressions that can't be built with And/Or.
.golangci.yaml (1)

55-56: Justify disabling the unused linter.

The unused linter is explicitly disabled without explanation. This linter identifies unused constants, variables, functions, and types, which is valuable for code cleanliness. If there's a specific reason for disabling it (e.g., known false positives), document it with a comment.

Consider re-enabling unless there's a strong reason:

   disable:
-    - unused
+    # - unused  # Disabled because: [reason]
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cccf6f8 and 77791c8.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (24)
  • .github/workflows/ci.yml (1 hunks)
  • .gitignore (1 hunks)
  • .golangci.yaml (1 hunks)
  • LICENSE (1 hunks)
  • README.md (1 hunks)
  • attachment.go (1 hunks)
  • attachment_test.go (1 hunks)
  • auth.go (1 hunks)
  • client.go (1 hunks)
  • client_test.go (1 hunks)
  • entry.go (1 hunks)
  • entry_test.go (1 hunks)
  • errors.go (1 hunks)
  • go.mod (1 hunks)
  • interfaces.go (1 hunks)
  • internal/queue/queue.go (1 hunks)
  • internal/queue/queue_test.go (1 hunks)
  • internal/ratelimit/ratelimit.go (1 hunks)
  • internal/ratelimit/ratelimit_test.go (1 hunks)
  • options.go (1 hunks)
  • query.go (1 hunks)
  • query_test.go (1 hunks)
  • token_refresh_test.go (1 hunks)
  • types.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (13)
client_test.go (4)
client.go (1)
  • New (94-113)
options.go (8)
  • WithHTTPClient (18-22)
  • QueryOption (66-66)
  • WithFields (80-84)
  • WithQualification (87-91)
  • WithSort (94-99)
  • WithLimit (102-106)
  • WithOffset (109-113)
  • WithExpand (116-120)
errors.go (4)
  • APIError (35-50)
  • ErrUnauthorized (12-12)
  • ErrForbidden (15-15)
  • ErrNotFound (18-18)
types.go (2)
  • SortAsc (33-33)
  • SortDesc (35-35)
internal/queue/queue_test.go (3)
client.go (1)
  • New (94-113)
internal/queue/queue.go (2)
  • New (27-32)
  • ErrQueueClosed (16-16)
options.go (1)
  • WithTimeout (26-30)
query_test.go (1)
query.go (1)
  • NewQuery (35-37)
interfaces.go (2)
options.go (1)
  • QueryOption (66-66)
types.go (3)
  • Entry (4-7)
  • EntryList (10-13)
  • DeleteOption (39-39)
errors.go (3)
client.go (1)
  • New (94-113)
internal/queue/queue.go (1)
  • New (27-32)
internal/ratelimit/ratelimit.go (1)
  • New (23-30)
client.go (5)
interfaces.go (3)
  • HTTPDoer (11-13)
  • EntryServicer (17-35)
  • AttachmentServicer (39-45)
internal/ratelimit/ratelimit.go (2)
  • Limiter (12-18)
  • New (23-30)
internal/queue/queue.go (2)
  • Queue (20-24)
  • New (27-32)
options.go (2)
  • Option (13-13)
  • WithTimeout (26-30)
errors.go (2)
  • ErrNoCredentials (24-24)
  • APIError (35-50)
token_refresh_test.go (6)
client.go (1)
  • New (94-113)
internal/queue/queue.go (1)
  • New (27-32)
internal/ratelimit/ratelimit.go (1)
  • New (23-30)
options.go (4)
  • WithHTTPClient (18-22)
  • WithTokenLifetime (42-46)
  • WithRefreshThreshold (50-54)
  • WithAutoRefresh (59-63)
types.go (1)
  • Entry (4-7)
errors.go (2)
  • ErrNoCredentials (24-24)
  • APIError (35-50)
attachment.go (1)
client.go (1)
  • Client (67-91)
auth.go (3)
client.go (2)
  • New (94-113)
  • Client (67-91)
internal/queue/queue.go (1)
  • New (27-32)
internal/ratelimit/ratelimit.go (1)
  • New (23-30)
entry_test.go (5)
client.go (2)
  • Client (67-91)
  • New (94-113)
options.go (2)
  • WithFields (80-84)
  • WithQualification (87-91)
types.go (4)
  • Entry (4-7)
  • EntryList (10-13)
  • DeleteOptionForce (45-45)
  • DeleteOption (39-39)
query.go (2)
  • Query (18-21)
  • NewQuery (35-37)
errors.go (3)
  • ErrNotFound (18-18)
  • ErrEmptyFormName (27-27)
  • ErrEmptyEntryID (30-30)
entry.go (4)
client.go (1)
  • Client (67-91)
options.go (1)
  • QueryOption (66-66)
types.go (3)
  • Entry (4-7)
  • EntryList (10-13)
  • DeleteOption (39-39)
errors.go (2)
  • ErrEmptyFormName (27-27)
  • ErrEmptyEntryID (30-30)
options.go (4)
client.go (2)
  • Client (67-91)
  • New (94-113)
interfaces.go (1)
  • HTTPDoer (11-13)
internal/ratelimit/ratelimit.go (1)
  • New (23-30)
types.go (2)
  • SortOrder (29-29)
  • SortDesc (35-35)
attachment_test.go (2)
client.go (1)
  • New (94-113)
errors.go (1)
  • APIError (35-50)
🔇 Additional comments (78)
LICENSE (1)

1-21: LGTM!

Standard MIT License with proper copyright notice.

.gitignore (1)

1-29: LGTM!

Comprehensive .gitignore covering build artifacts, test outputs, IDE files, and generated content.

README.md (1)

1-296: LGTM!

Well-structured documentation with comprehensive coverage of all library features, including authentication flows, entry operations, query builder syntax, attachment handling, and error handling patterns. The testing section appropriately highlights the interface-based design for mockability.

attachment_test.go (3)

15-42: Well-designed test utility.

The closeTrackingReader correctly simulates real HTTP response body behavior by failing reads after close, which helps catch bugs where code attempts to read from a closed body.


44-74: LGTM!

Test properly verifies that API error details are preserved and returned rather than generic HTTP status messages. Good use of closeTrackingReader to catch potential read-after-close bugs.


100-121: LGTM!

Upload success test correctly drains the request body to prevent blocking the pipe writer goroutine. The comment on line 105-106 is helpful for understanding this requirement.

attachment.go (2)

17-51: LGTM!

The Get method correctly:

  1. Parses the API error before closing the response body (lines 39-43)
  2. Returns an attachmentReader wrapper that cancels the request context when closed
  3. Properly cleans up resources on error paths

135-146: LGTM!

Clean wrapper pattern that ensures the request context is cancelled when the caller closes the reader, preventing resource leaks for long-lived connections.

query_test.go (3)

10-69: LGTM!

Comprehensive table-driven tests covering various value types (strings, integers, booleans, nil) with correct AR System qualification syntax.


112-131: Excellent security-focused tests.

Good coverage of injection prevention through field name escaping (lines 112-121) and operator validation (lines 123-131). The single quote escaping test verifies that malicious field names like Status' OR '1'='1 are safely escaped.


147-165: LGTM!

Good coverage of AR System field naming conventions, including spaces, colons, parentheses, dashes, and underscores.

token_refresh_test.go (4)

19-37: LGTM!

Clear test verifying that credentials are stored after successful login for auto-refresh functionality.


222-270: LGTM!

Excellent test for verifying that concurrent requests trigger only a single token refresh. The use of atomic.Int32 for counting login calls and proper synchronization with WaitGroup ensures accurate testing of the double-check locking pattern.


428-469: LGTM!

Good test for context cancellation propagation during token refresh. Properly verifies that cancelled contexts are respected in the refresh flow.


254-264: sync.WaitGroup.Go() requires Go 1.25.

The wg.Go(func() { ... }) pattern introduced in Go 1.25 provides a new method for managing goroutines within a WaitGroup. This aligns with the go 1.25 directive in go.mod, but ensure this is intentional, as users on earlier Go versions will encounter compilation errors.

go.mod (1)

3-3: Go 1.25 is a valid released version. Go 1.25 was released on August 12, 2025 and is currently stable. No action needed.

auth.go (6)

1-19: LGTM on imports and token size guard.

The 64KB token size limit is reasonable headroom for JWTs while preventing memory exhaustion attacks. The sentinel error pattern with ErrTokenTooLarge follows Go conventions.


21-46: LGTM on Login flow.

Clean delegation from Login to LoginWithAuth. Credentials are stored only after successful authentication, and the queue is properly released via defer.


48-63: LGTM on queue and rate limiter coordination.

Correctly acquires queue first, then rate limiter, with proper cleanup if rate limiting fails.


65-110: LGTM on loginInternal implementation.

The implementation correctly:

  • Uses io.LimitReader with maxTokenSize+1 to detect oversized tokens
  • Defers both cancel function and response body close
  • Wraps errors with context
  • Trims whitespace from the token before storing

112-148: LGTM on Logout implementation.

Defensive token clearance on both success and failure paths ensures the client doesn't retain stale credentials. Accepting both 200 OK and 204 No Content is appropriate for logout endpoints.


150-154: LGTM on IsAuthenticated.

The comment correctly clarifies the method's behavior - it checks token presence, not validity. Token expiry handling is managed separately by the auto-refresh mechanism.

entry_test.go (6)

14-37: LGTM on setupAuthenticatedClient helper.

Clean test helper that simulates login on first call and delegates subsequent calls to the provided function. Uses t.Helper() correctly for proper test failure attribution.


39-94: LGTM on Get tests.

Good coverage including the authorization header check, query options, and proper APIError.Is() assertion for 404 responses.


96-175: LGTM on List, Create, and Update tests.

Thorough validation of HTTP methods, paths, headers, and request/response bodies.


200-217: Excellent security test for query parameter injection.

Testing that malicious option values like "FORCE&evil=1" are properly URL-encoded to "FORCE%26evil%3D1" is a valuable security verification.


258-274: Good path traversal prevention test.

Verifying that slashes in form names are encoded as %2F prevents potential path traversal attacks when form names contain forward slashes.


276-355: LGTM on validation error tests.

Complete coverage of input validation for empty form names and entry IDs across all operations. Using unauthenticated clients confirms validation happens before authentication checks.

errors.go (3)

9-31: LGTM on sentinel errors.

Well-organized sentinel errors with clear documentation. The separation between HTTP-related errors (Unauthorized, Forbidden, NotFound) and application-level errors (NotAuthenticated, NoCredentials, EmptyFormName, EmptyEntryID) is appropriate.


63-76: LGTM on APIError.Is() implementation.

The Is() method correctly implements the errors.Is interface, allowing callers to check errors.Is(apiErr, ErrNotFound) which maps to status code 404. The use of errors.Is(target, ErrX) in the switch handles wrapped targets correctly.


52-61: LGTM on Error() formatting.

Clear and consistent error message format that includes all diagnostic information (type, number, text, and optional appended text).

client_test.go (7)

15-42: LGTM on test helpers.

mockHTTPClient and newMockResponse are clean utilities. The panic in newMockResponse on marshal error is acceptable for test code since it indicates a test bug.


44-57: LGTM on New constructor tests.

Validates client initialization and trailing slash normalization in base URL.


59-146: LGTM on authentication flow tests.

Good coverage of login success/failure, logout with proper auth header, and idempotent logout when not authenticated.


148-222: LGTM on APIError tests.

Table-driven tests thoroughly cover error string formatting and Is() method behavior for all mapped status codes.


224-283: LGTM on query parameter tests.

Comprehensive coverage of all query options with proper value formatting.


306-326: LGTM on input validation edge cases.

Good defensive testing for negative and zero values in limit/offset parameters.


328-349: LGTM on oversized token test.

Testing rejection of a 2MB token response validates the memory exhaustion protection implemented in loginInternal.

types.go (3)

3-27: LGTM on core data types.

Clean struct definitions with appropriate JSON tags. Using omitzero on Links fields prevents empty arrays in JSON output.


28-48: LGTM on enumeration types.

Type-safe string wrappers for sort orders and delete options with well-documented constants.


50-57: LGTM on apiErrorResponse.

Correctly unexported internal type for parsing BMC Remedy API error responses with appropriate JSON field mappings.

interfaces.go (4)

9-13: LGTM!

The HTTPDoer interface is a clean abstraction for testing, allowing easy mocking of HTTP clients.


15-35: LGTM!

The EntryServicer interface provides a complete CRUD API with proper context support and idiomatic option patterns.


37-45: LGTM!

The AttachmentServicer interface uses appropriate streaming types (io.ReadCloser, io.Reader) for memory-efficient attachment handling.


47-64: LGTM!

The RemedyClient interface provides a complete and testable API surface with proper context support and flexible authentication options.

query.go (5)

9-37: LGTM!

The Query builder design is clean with helpful documentation. The example usage clearly demonstrates the fluent API pattern.


39-69: LGTM!

The distinction between safe (validated) and unsafe (unvalidated) methods provides flexibility. Capturing only the first error is a reasonable design choice for fluent APIs.


82-94: LGTM!

Build and BuildSafe provide appropriate options for checked and unchecked query building.


151-160: LGTM!

The exported operator constants provide a user-friendly API with IDE support and prevent typos.


128-149: String value formatting uses incorrect escape mechanism for AR System qualifications.

AR System uses quote doubling for escaped delimiters within expressions (e.g., field names with apostrophes are written as 'Doug''s Requests'). String values must be enclosed in double quotes, which means special characters within values should follow the same quote-doubling pattern. However, line 132 uses fmt.Sprintf("%q", val), which produces Go-style backslash escaping instead of quote doubling. A value containing quotes like test"value would be formatted as "test\"value" by %q, but AR System likely expects "test""value".

options.go (3)

12-63: LGTM!

The functional options pattern is well-implemented with clear documentation including default values. This provides a clean, extensible API for client configuration.


65-120: LGTM!

The QueryOption pattern provides a flexible and type-safe way to configure entry queries with all common parameters supported.


122-160: Fields parameter format is correct; sort parameter documentation is sparse in Remedy API.

The fields parameter format using values(field1,field2,...) is confirmed in BMC Remedy REST API documentation. However, the sort parameter documentation is notably sparse. The .desc suffix convention appears reasonable, but I recommend verifying the exact sort format with your Remedy instance or BMC documentation if descending sorts are critical to your use case.

internal/ratelimit/ratelimit_test.go (1)

1-158: LGTM!

Comprehensive test suite with excellent coverage of:

  • Core rate limiting behavior (Allow, Wait)
  • Context handling (cancellation, timeout)
  • Edge cases (token capping, immediate return)
  • Concurrency safety (race conditions)

The tests demonstrate thorough validation of the rate limiter implementation.

entry.go (6)

15-47: LGTM!

The Get method properly validates inputs, manages queue resources with defer, and handles errors with context wrapping.


49-78: LGTM!

The List method follows the same robust pattern as Get with proper validation, resource management, and error handling.


80-104: LGTM!

The Create method correctly wraps field values in the expected JSON structure and follows the established pattern for resource management.


106-132: LGTM!

The Update method uses the correct HTTP method (PUT) for updating existing entries and properly handles the no-content response.


134-164: LGTM!

The Delete method correctly handles optional delete options, using only the first option which is appropriate for a singular operation parameter.


166-191: LGTM!

The Merge method implements the create-or-update semantics correctly with a POST to the merge endpoint. The use of apiBasePath constant (defined elsewhere) maintains consistency with other path builders.

internal/queue/queue_test.go (1)

1-155: LGTM!

Excellent test coverage of the queue synchronization primitive:

  • Serialization guarantees (max concurrent = 1)
  • Context handling with proper error checking
  • Lifecycle management (close, idempotency)
  • Safety guarantees (panic on invalid release, safe double-close)

The tests provide high confidence in the queue's correctness and thread-safety.

internal/ratelimit/ratelimit.go (3)

34-46: LGTM!

The Allow() method correctly acquires the mutex, refills tokens, and atomically checks/consumes a token.


50-66: LGTM!

The atomic acquire-or-wait pattern correctly prevents TOCTOU races. Context cancellation is properly handled.


68-98: LGTM!

The tryAcquireOrGetWaitTime() and refill() implementations are correct. The mutex contract for refill() is properly documented.

client.go (11)

51-91: LGTM!

Good defensive design with private credential fields to prevent accidental logging via %+v. The TODO comments for Go 1.26's runtime/secret are forward-looking. The mutex separation (tokenMu, credentialsMu, refreshMu) correctly partitions concerns.


93-113: LGTM!

Constructor correctly applies options before initializing services, ensuring configuration is finalized first.


115-128: LGTM!

Interface-returning accessors enable easier testing. Close() correctly only closes the queue, not the httpClient, which may be shared.


130-161: LGTM!

Token accessors correctly use RLock for reads and Lock for writes, ensuring thread-safe access.


163-190: LGTM!

Credential management is thread-safe. Exporting ClearCredentials allows users to explicitly purge sensitive data from memory.


209-233: LGTM!

The double-check locking pattern is correctly implemented: fast-path read check, then serialize with refreshMu, then recheck before actually refreshing. This prevents thundering herd on token refresh.


249-269: LGTM!

Resource management is correct: queue is released if rate limiting fails. Token validation before queue acquisition avoids holding the queue during potentially slow token refresh.


271-314: LGTM!

Request creation correctly manages the cancel function: it's cancelled on error in newRequest and returned for caller management otherwise. JSON headers are properly set.


316-357: LGTM!

Response handling correctly defers cleanup (cancel function and body close), handles 204 No Content, and routes errors to parseAPIError.


359-390: LGTM!

Error parsing gracefully handles unparseable responses and empty error arrays with sensible fallbacks.


392-400: LGTM!

Path helpers correctly use url.PathEscape for form names and entry IDs, preventing injection and encoding issues.

internal/queue/queue.go (3)

1-32: LGTM!

Clear documentation explaining the motivation (BMC Error 9093). The 1-slot semaphore pattern correctly enforces single-request serialization.


34-60: LGTM!

The pre-checks before the main select correctly ensure priority: context errors first, then closed-queue detection, before waiting. This prevents the non-deterministic select from acquiring a semaphore on an already-closed queue.


62-80: LGTM!

Release() panics on misuse, which is appropriate for a programming error (Release without Acquire). Close() is correctly idempotent via sync.Once.

- ratelimit: Validate requestsPerSecond > 0 to prevent division by zero
- attachment: Clean up multipart writer on error paths
@tphakala tphakala merged commit 233be69 into main Dec 17, 2025
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant