Skip to content

feat: add buzz api command for authenticated Beeminder API requests#228

Open
Copilot wants to merge 3 commits intomainfrom
copilot/add-buzz-api-command
Open

feat: add buzz api command for authenticated Beeminder API requests#228
Copilot wants to merge 3 commits intomainfrom
copilot/add-buzz-api-command

Conversation

Copy link
Contributor

Copilot AI commented Feb 23, 2026

  • Explore codebase and understand architecture
  • Add handleApiCommand() function to main.go
  • Add makeAPIRequest() helper (testable core logic) to main.go
  • Add "api" case to main switch statement
  • Update printHelp() with api command docs
  • Update default case error message to include api
  • Add imports (io, net/http, net/url) to main.go
  • Write tests in api_test.go
  • Use url.PathEscape for {user} substitution in path
  • Add tests for PUT, DELETE, and PATCH methods
  • Add test for usernames with special URL characters
  • All 11 tests pass, CodeQL shows 0 alerts
Original prompt

This section details on the original issue you should resolve

<issue_title>Add buzz api command for using the beeminder api via the buzz cli, using buzz cli auth</issue_title>
<issue_description></issue_description>

Comments on the Issue (you are @copilot in this section)


🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.

Co-authored-by: narthur <4655422+narthur@users.noreply.github.com>
Copilot AI changed the title [WIP] Add buzz api command for beeminder api using buzz cli feat: add buzz api command for authenticated Beeminder API requests Feb 23, 2026
Copilot AI requested a review from narthur February 23, 2026 15:51
@narthur narthur marked this pull request as ready for review February 23, 2026 16:30
Copilot AI review requested due to automatic review settings February 23, 2026 16:30
@github-actions
Copy link

github-actions bot commented Feb 23, 2026

🚀 Pre-release Build Ready

Test builds are ready! Install directly using the bin command:

# Install the pre-release
bin install https://github.com/PinePeakDigital/buzz/releases/tag/pr-228-latest buzz-pr-228
# Run the pre-release
buzz-pr-228
# Uninstall the pre-release
bin remove buzz-pr-228

Direct Download Links

Or download binaries directly from the pre-release page:

💡 No GitHub login required for downloads!

🗑️ This pre-release will be automatically deleted when the PR is closed.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new buzz api command that provides a general-purpose interface for making authenticated HTTP requests to the Beeminder API. The command uses credentials stored in ~/.buzzrc and automatically injects the auth token, eliminating the need for manual authentication token handling.

Changes:

  • Adds a new buzz api <method> <path> [key=value ...] command supporting GET, POST, PUT, DELETE, and PATCH methods with automatic auth token injection and username substitution via {user} placeholder
  • Implements testable makeAPIRequest() function with proper HTTP method routing (query strings for GET/DELETE, form body for POST/PUT/PATCH)
  • Includes comprehensive test coverage for core functionality including GET/POST methods, parameter handling, user placeholder substitution, and JSON response handling

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
main.go Added makeAPIRequest() core function and handleApiCommand() handler; integrated api command into dispatch switch; updated help text and command list; added necessary imports (io, net/http, net/url)
api_test.go Added 7 unit tests covering GET/POST request dispatch, {user} placeholder substitution, parameter injection to query strings and request bodies, invalid parameter format handling, and non-JSON response handling

main.go Outdated
Comment on lines +1632 to +1633
// Replace {user} placeholder with the configured username
path = strings.ReplaceAll(path, "{user}", config.Username)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The username substitution in the path doesn't use URL escaping, which could cause issues if the username contains special URL characters. Other parts of the codebase use url.PathEscape() for similar path substitutions (see beeminder.go:520, 577, 650). Consider using strings.ReplaceAll(path, "{user}", url.PathEscape(config.Username)) instead.

Suggested change
// Replace {user} placeholder with the configured username
path = strings.ReplaceAll(path, "{user}", config.Username)
// Replace {user} placeholder with the configured username, escaping it for use in the URL path
path = strings.ReplaceAll(path, "{user}", url.PathEscape(config.Username))

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 7b7a166{user} is now replaced using url.PathEscape(config.Username), consistent with how other path substitutions work in beeminder.go.

t.Errorf("value missing from body; body = %q", capturedBody)
}
}

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The tests cover GET and POST methods but don't test PUT, DELETE, or PATCH methods. Since the command supports all five HTTP methods (line 1707-1709 in main.go), consider adding tests for the remaining three methods to ensure they correctly send parameters in the request body rather than query string.

Suggested change
// TestMakeAPIRequestPUT tests that PUT params are sent in the request body
func TestMakeAPIRequestPUT(t *testing.T) {
var capturedBody string
var capturedQuery string
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPut {
t.Errorf("expected PUT, got %s", r.Method)
}
capturedQuery = r.URL.RawQuery
if err := r.ParseForm(); err != nil {
t.Fatalf("failed to parse form: %v", err)
}
capturedBody = r.PostForm.Encode()
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"id":"dp1"}`)
}))
defer mockServer.Close()
config := &Config{
Username: "alice",
AuthToken: "tok123",
BaseURL: mockServer.URL,
}
_, status, err := makeAPIRequest(config, "PUT",
"/api/v1/users/alice/goals/pushups/datapoints.json",
[]string{"value=5", "comment=test"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status != http.StatusOK {
t.Errorf("status = %d, want 200", status)
}
if !strings.Contains(capturedBody, "auth_token=tok123") {
t.Errorf("auth_token missing from body; body = %q", capturedBody)
}
if !strings.Contains(capturedBody, "value=5") {
t.Errorf("value missing from body; body = %q", capturedBody)
}
if strings.Contains(capturedQuery, "value=5") {
t.Errorf("value should not be in query string; query = %q", capturedQuery)
}
}
// TestMakeAPIRequestDELETE tests that DELETE params are sent in the request body
func TestMakeAPIRequestDELETE(t *testing.T) {
var capturedBody string
var capturedQuery string
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
capturedQuery = r.URL.RawQuery
if err := r.ParseForm(); err != nil {
t.Fatalf("failed to parse form: %v", err)
}
capturedBody = r.PostForm.Encode()
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"id":"dp1"}`)
}))
defer mockServer.Close()
config := &Config{
Username: "alice",
AuthToken: "tok123",
BaseURL: mockServer.URL,
}
_, status, err := makeAPIRequest(config, "DELETE",
"/api/v1/users/alice/goals/pushups/datapoints.json",
[]string{"value=5", "comment=test"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status != http.StatusOK {
t.Errorf("status = %d, want 200", status)
}
if !strings.Contains(capturedBody, "auth_token=tok123") {
t.Errorf("auth_token missing from body; body = %q", capturedBody)
}
if !strings.Contains(capturedBody, "value=5") {
t.Errorf("value missing from body; body = %q", capturedBody)
}
if strings.Contains(capturedQuery, "value=5") {
t.Errorf("value should not be in query string; query = %q", capturedQuery)
}
}
// TestMakeAPIRequestPATCH tests that PATCH params are sent in the request body
func TestMakeAPIRequestPATCH(t *testing.T) {
var capturedBody string
var capturedQuery string
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch {
t.Errorf("expected PATCH, got %s", r.Method)
}
capturedQuery = r.URL.RawQuery
if err := r.ParseForm(); err != nil {
t.Fatalf("failed to parse form: %v", err)
}
capturedBody = r.PostForm.Encode()
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"id":"dp1"}`)
}))
defer mockServer.Close()
config := &Config{
Username: "alice",
AuthToken: "tok123",
BaseURL: mockServer.URL,
}
_, status, err := makeAPIRequest(config, "PATCH",
"/api/v1/users/alice/goals/pushups/datapoints.json",
[]string{"value=5", "comment=test"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if status != http.StatusOK {
t.Errorf("status = %d, want 200", status)
}
if !strings.Contains(capturedBody, "auth_token=tok123") {
t.Errorf("auth_token missing from body; body = %q", capturedBody)
}
if !strings.Contains(capturedBody, "value=5") {
t.Errorf("value missing from body; body = %q", capturedBody)
}
if strings.Contains(capturedQuery, "value=5") {
t.Errorf("value should not be in query string; query = %q", capturedQuery)
}
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tests for PUT, DELETE, and PATCH in 7b7a166. The DELETE test verifies params go to the query string; PUT and PATCH tests verify params are sent in the body (not the query string).

t.Errorf("path = %q, want /api/v1/users/bob/goals.json", capturedPath)
}
}

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

There is no test coverage for usernames containing special URL characters (e.g., spaces, ampersands, slashes) when using the {user} placeholder. Given that the username substitution doesn't use URL escaping (see comment on main.go:1633), adding a test for this would help verify the fix when it's implemented.

Suggested change
// TestMakeAPIRequestUserPlaceholderSpecialChars tests {user} replacement when the username contains special URL characters.
func TestMakeAPIRequestUserPlaceholderSpecialChars(t *testing.T) {
var capturedPath string
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedPath = r.URL.Path
fmt.Fprintln(w, `{}`)
}))
defer mockServer.Close()
config := &Config{
Username: "bob smith&sons/extra",
AuthToken: "tok",
BaseURL: mockServer.URL,
}
if _, _, err := makeAPIRequest(config, "GET", "/api/v1/users/{user}/goals.json", nil); err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedPath := "/api/v1/users/bob smith&sons/extra/goals.json"
if capturedPath != expectedPath {
t.Errorf("path = %q, want %q", capturedPath, expectedPath)
}
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added TestMakeAPIRequestUserPlaceholderSpecialChars in 7b7a166 — it verifies that a username with a space is encoded as bob%20smith in the request URI after the url.PathEscape fix.

@narthur
Copy link
Collaborator

narthur commented Feb 24, 2026

@copilot apply changes based on the comments in this thread

…/special-chars tests

Co-authored-by: narthur <4655422+narthur@users.noreply.github.com>
Copilot AI requested a review from narthur February 24, 2026 17:44
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.

Add buzz api command for using the beeminder api via the buzz cli, using buzz cli auth

3 participants