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
352 changes: 120 additions & 232 deletions internal/tiger/api/client.go

Large diffs are not rendered by default.

34 changes: 24 additions & 10 deletions internal/tiger/api/client_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"context"
"encoding/base64"
"errors"
"fmt"
"net/http"
"sync"
Expand Down Expand Up @@ -92,16 +93,29 @@ func ValidateAPIKeyWithClient(client ClientWithResponsesInterface, projectID str
}

// Check the response status
switch resp.StatusCode() {
case 401, 403:
return fmt.Errorf("invalid API key: authentication failed")
case 404:
// Project not found is OK - it means the API key is valid but project doesn't exist
return nil
case 200:
// Success - API key is valid
if resp.StatusCode() != 200 {
if resp.StatusCode() == 404 {
// Project not found, but API key is valid
return nil
}
if resp.JSON4XX != nil {
return resp.JSON4XX
} else {
return errors.New("unexpected API response: 500")
}
} else {
return nil
default:
return fmt.Errorf("unexpected API response: %d", resp.StatusCode())
}
}

// Error implements the error interface for the Error type.
// This allows Error values to be used directly as Go errors.
func (e *Error) Error() string {
if e == nil {
return "unknown error"
}
if e.Message != nil && *e.Message != "" {
return *e.Message
}
return "unknown error"
}
7 changes: 5 additions & 2 deletions internal/tiger/api/client_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http"
"testing"

"github.com/timescale/tiger-cli/internal/tiger/util"
"go.uber.org/mock/gomock"

"github.com/timescale/tiger-cli/internal/tiger/api"
Expand Down Expand Up @@ -46,9 +47,10 @@ func TestValidateAPIKeyWithClient(t *testing.T) {
GetProjectsProjectIdServicesWithResponse(gomock.Any(), "00000000-0000-0000-0000-000000000000").
Return(&api.GetProjectsProjectIdServicesResponse{
HTTPResponse: &http.Response{StatusCode: 401},
JSON4XX: &api.ClientError{Message: util.Ptr("Invalid or missing authentication credentials")},
}, nil)
},
expectedError: "invalid API key: authentication failed",
expectedError: "Invalid or missing authentication credentials",
},
{
name: "invalid API key - 403 response",
Expand All @@ -57,9 +59,10 @@ func TestValidateAPIKeyWithClient(t *testing.T) {
GetProjectsProjectIdServicesWithResponse(gomock.Any(), "00000000-0000-0000-0000-000000000000").
Return(&api.GetProjectsProjectIdServicesResponse{
HTTPResponse: &http.Response{StatusCode: 403},
JSON4XX: &api.ClientError{Message: util.Ptr("Invalid or missing authentication credentials")},
}, nil)
},
expectedError: "invalid API key: authentication failed",
expectedError: "Invalid or missing authentication credentials",
},
{
name: "unexpected response - 500",
Expand Down
10 changes: 2 additions & 8 deletions internal/tiger/api/types.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 7 additions & 15 deletions internal/tiger/cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,23 +321,15 @@ func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) {
}

// Handle API response
switch resp.StatusCode() {
case 200:
if resp.JSON200 == nil {
return api.Service{}, fmt.Errorf("empty response from API")
}

return *resp.JSON200, nil
if resp.StatusCode() != 200 {
return api.Service{}, exitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
}

case 401:
return api.Service{}, exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication failed: invalid API key"))
case 403:
return api.Service{}, exitWithCode(ExitPermissionDenied, fmt.Errorf("permission denied: insufficient access to service"))
case 404:
return api.Service{}, exitWithCode(ExitServiceNotFound, fmt.Errorf("service '%s' not found in project '%s'", serviceID, projectID))
default:
return api.Service{}, fmt.Errorf("API request failed with status %d", resp.StatusCode())
if resp.JSON200 == nil {
return api.Service{}, fmt.Errorf("empty response from API")
}

return *resp.JSON200, nil
}

// ArgsLenAtDashProvider defines the interface for getting ArgsLenAtDash
Expand Down
33 changes: 33 additions & 0 deletions internal/tiger/cmd/errors.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package cmd

import "errors"

// Exit codes as defined in the CLI specification
const (
ExitSuccess = 0 // Success
Expand Down Expand Up @@ -32,3 +34,34 @@ func (e exitCodeError) ExitCode() int {
func exitWithCode(code int, err error) error {
return exitCodeError{code: code, err: err}
}

// exitWithErrorFromStatusCode maps HTTP status codes to CLI exit codes
func exitWithErrorFromStatusCode(statusCode int, err error) error {
if err == nil {
err = errors.New("unknown error")
}
switch statusCode {
case 400:
// Bad request - invalid parameters
return exitWithCode(ExitInvalidParameters, err)
case 401:
// Unauthorized - authentication error
return exitWithCode(ExitAuthenticationError, err)
case 403:
// Forbidden - permission denied
return exitWithCode(ExitPermissionDenied, err)
case 404:
// Not found - service/resource not found
return exitWithCode(ExitServiceNotFound, err)
case 408, 504:
// Request timeout or gateway timeout
return exitWithCode(ExitTimeout, err)
default:
// For other 4xx errors, use general error
if statusCode >= 400 && statusCode < 500 {
return exitWithCode(ExitGeneralError, err)
}
// For 5xx and other errors, use general error
return exitWithCode(ExitGeneralError, err)
}
}
8 changes: 6 additions & 2 deletions internal/tiger/cmd/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func executeIntegrationCommand(args ...string) (string, error) {
// TestServiceLifecycleIntegration tests the complete authentication and service lifecycle:
// login -> whoami -> create -> describe -> update-password -> delete -> logout
func TestServiceLifecycleIntegration(t *testing.T) {
config.SetTestServiceName(t)
// Check for required environment variables
publicKey := os.Getenv("TIGER_PUBLIC_KEY_INTEGRATION")
secretKey := os.Getenv("TIGER_SECRET_KEY_INTEGRATION")
Expand Down Expand Up @@ -399,8 +400,8 @@ func TestServiceLifecycleIntegration(t *testing.T) {
}

// Check that error indicates service not found
if !strings.Contains(err.Error(), "not found") && !strings.Contains(err.Error(), "404") {
t.Errorf("Expected 'not found' error for deleted service, got: %v", err)
if !strings.Contains(err.Error(), "no service with that id exists") {
t.Errorf("Expected 'no service with that id exists' error for deleted service, got: %v", err)
}

// Check that it returns the correct exit code (this should be required)
Expand Down Expand Up @@ -489,6 +490,7 @@ func TestServiceNotFound(t *testing.T) {
publicKey := os.Getenv("TIGER_PUBLIC_KEY_INTEGRATION")
secretKey := os.Getenv("TIGER_SECRET_KEY_INTEGRATION")
projectID := os.Getenv("TIGER_PROJECT_ID_INTEGRATION")
config.SetTestServiceName(t)

if publicKey == "" || secretKey == "" || projectID == "" {
t.Skip("Skipping service not found test: TIGER_PUBLIC_KEY_INTEGRATION, TIGER_SECRET_KEY_INTEGRATION, and TIGER_PROJECT_ID_INTEGRATION must be set")
Expand Down Expand Up @@ -602,6 +604,7 @@ func TestDatabaseCommandsIntegration(t *testing.T) {
secretKey := os.Getenv("TIGER_SECRET_KEY_INTEGRATION")
projectID := os.Getenv("TIGER_PROJECT_ID_INTEGRATION")
existingServiceID := os.Getenv("TIGER_EXISTING_SERVICE_ID_INTEGRATION") // Optional: use existing service
config.SetTestServiceName(t)

if publicKey == "" || secretKey == "" || projectID == "" {
t.Skip("Skipping integration test: TIGER_PUBLIC_KEY_INTEGRATION, TIGER_SECRET_KEY_INTEGRATION, and TIGER_PROJECT_ID_INTEGRATION must be set")
Expand Down Expand Up @@ -666,6 +669,7 @@ func TestAuthenticationErrorsIntegration(t *testing.T) {
publicKey := os.Getenv("TIGER_PUBLIC_KEY_INTEGRATION")
secretKey := os.Getenv("TIGER_SECRET_KEY_INTEGRATION")
projectID := os.Getenv("TIGER_PROJECT_ID_INTEGRATION")
config.SetTestServiceName(t)

if publicKey == "" || secretKey == "" || projectID == "" {
t.Skip("Skipping authentication error integration test: TIGER_PUBLIC_KEY_INTEGRATION, TIGER_SECRET_KEY_INTEGRATION, and TIGER_PROJECT_ID_INTEGRATION must be set")
Expand Down
Loading