Skip to content

Commit 861b9da

Browse files
cvclaude
andcommitted
refactor: Reduce cyclomatic complexity and enable cyclop/gochecknoglobals
API layer (4 functions): - Extract allDoorsLocked, validateLoginResponse helpers - Extract handleRetryableError, prepareRequestParams, calculateSignature, buildHTTPRequest CLI layer (6 functions): - Extract buildTimeoutMessage, applyInitialDelay helpers - Use map-based dispatch for status formatting - Split displayAllStatus into JSON/text variants - Extract formatMinutesDuration, use windowPosition struct CLI tests (2 functions): - Extract setupMockForWaitCondition, verifyWaitConditionResult - Extract setupMockForHvacOn, verifyHvacOnResult Config: - Remove blanket cyclop exclusion, enable for api/cli - Add specific gochecknoglobals exclusions for justified globals 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 146d885 commit 861b9da

9 files changed

Lines changed: 469 additions & 306 deletions

File tree

.golangci.yml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,35 @@ linters:
7676
- goconst
7777
# TODO(mcs-e0yb): Temporarily exclude new linters until refactored
7878
# Cyclomatic complexity - excluded paths that still need refactoring
79-
- path: (cmd/|internal/cache/|internal/cli/|internal/config/|internal/crypto/|internal/sensordata/)
79+
- path: (cmd/|internal/cache/|internal/config/|internal/crypto/|internal/sensordata/)
8080
linters:
8181
- cyclop
82-
# Global variables - 9 globals need evaluation
83-
- path: \.go
82+
83+
# Global variables - justified exclusions:
84+
# 1. Read-only lookup tables (RegionConfigs, screenSizes, androidVersionToSDK)
85+
- path: internal/api/auth\.go
86+
linters:
87+
- gochecknoglobals
88+
text: RegionConfigs
89+
- path: internal/sensordata/system_info\.go
90+
linters:
91+
- gochecknoglobals
92+
text: (screenSizes|androidVersionToSDK)
93+
94+
# 2. CLI flags bound by Cobra framework (Version, ConfigFile, NoColor)
95+
- path: internal/cli/root\.go
96+
linters:
97+
- gochecknoglobals
98+
text: (Version|ConfigFile|NoColor)
99+
100+
# 3. Color state management - global state needed for CLI output formatting
101+
- path: internal/cli/color\.go
102+
linters:
103+
- gochecknoglobals
104+
text: (colorEnabled|colorMu)
105+
106+
# 4. Test helper mutex for serializing parallel tests
107+
- path: internal/cli/color_test\.go
84108
linters:
85109
- gochecknoglobals
110+
text: colorTestMutex

internal/api/auth.go

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,24 @@ func (c *Client) GetUsherEncryptionKey(ctx context.Context) (string, string, err
289289
return response.Data.PublicKey, response.Data.VersionPrefix, nil
290290
}
291291

292+
// validateLoginResponse checks the login response status and returns an error if invalid.
293+
func validateLoginResponse(response *LoginResponse) error {
294+
switch response.Status {
295+
case "INVALID_CREDENTIAL":
296+
return errors.New("invalid email or password")
297+
case "USER_LOCKED":
298+
return errors.New("account is locked")
299+
case "OK":
300+
if response.Data.AccessToken == "" {
301+
return errors.New("access token not found in response")
302+
}
303+
304+
return nil
305+
default:
306+
return fmt.Errorf("login failed with status: %s", response.Status)
307+
}
308+
}
309+
292310
// Login authenticates with the API and retrieves an access token.
293311
func (c *Client) Login(ctx context.Context) error {
294312
// Ensure we have a timeout for the request
@@ -346,18 +364,8 @@ func (c *Client) Login(ctx context.Context) error {
346364
return fmt.Errorf("failed to parse response: %w", err)
347365
}
348366

349-
if response.Status == "INVALID_CREDENTIAL" {
350-
return errors.New("invalid email or password")
351-
}
352-
if response.Status == "USER_LOCKED" {
353-
return errors.New("account is locked")
354-
}
355-
if response.Status != "OK" {
356-
return fmt.Errorf("login failed with status: %s", response.Status)
357-
}
358-
359-
if response.Data.AccessToken == "" {
360-
return errors.New("access token not found in response")
367+
if err := validateLoginResponse(&response); err != nil {
368+
return err
361369
}
362370

363371
c.accessToken = response.Data.AccessToken

internal/api/client.go

Lines changed: 120 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,48 @@ func (c *Client) APIRequestJSON(ctx context.Context, method, uri string, queryPa
5959
// retryFunc is the type for functions that can be retried.
6060
type retryFunc[T any] func(ctx context.Context, method, uri string, queryParams map[string]string, bodyParams map[string]any, needsKeys, needsAuth bool) (T, error)
6161

62+
// handleRetryableError attempts to recover from an encryption or token error by refreshing credentials.
63+
// Returns true if the error was handled and a retry should be attempted.
64+
func handleRetryableError[T any](
65+
ctx context.Context,
66+
c *Client,
67+
err error,
68+
retryCount int,
69+
) (shouldRetry bool, retryErr error) {
70+
var encErr *EncryptionError
71+
var tokenErr *TokenExpiredError
72+
73+
if errors.As(err, &encErr) {
74+
// Retrieve new encryption keys and retry
75+
if err := c.GetEncryptionKeys(ctx); err != nil {
76+
return false, fmt.Errorf("failed to retrieve encryption keys: %w", err)
77+
}
78+
// Apply backoff delay before retry
79+
backoff := calculateBackoff(retryCount + 1)
80+
if err := c.sleepFunc(ctx, backoff); err != nil {
81+
return false, err
82+
}
83+
84+
return true, nil
85+
}
86+
87+
if errors.As(err, &tokenErr) {
88+
// Login again and retry
89+
if err := c.Login(ctx); err != nil {
90+
return false, fmt.Errorf("failed to login: %w", err)
91+
}
92+
// Apply backoff delay before retry
93+
backoff := calculateBackoff(retryCount + 1)
94+
if err := c.sleepFunc(ctx, backoff); err != nil {
95+
return false, err
96+
}
97+
98+
return true, nil
99+
}
100+
101+
return false, nil
102+
}
103+
62104
// genericRetry implements the retry logic with exponential backoff for API requests.
63105
// It handles encryption errors and token expiration by refreshing credentials and retrying.
64106
func genericRetry[T any](
@@ -97,31 +139,11 @@ func genericRetry[T any](
97139
response, err := executeFunc(ctx, method, uri, queryParams, bodyParams, needsKeys, needsAuth)
98140
if err != nil {
99141
// Handle retryable errors
100-
var encErr *EncryptionError
101-
var tokenErr *TokenExpiredError
102-
if errors.As(err, &encErr) {
103-
// Retrieve new encryption keys and retry
104-
if err := c.GetEncryptionKeys(ctx); err != nil {
105-
return zero, fmt.Errorf("failed to retrieve encryption keys: %w", err)
106-
}
107-
// Apply backoff delay before retry
108-
backoff := calculateBackoff(retryCount + 1)
109-
if err := c.sleepFunc(ctx, backoff); err != nil {
110-
return zero, err
111-
}
112-
113-
return genericRetry(ctx, c, method, uri, queryParams, bodyParams, needsKeys, needsAuth, retryCount+1, executeFunc)
114-
} else if errors.As(err, &tokenErr) {
115-
// Login again and retry
116-
if err := c.Login(ctx); err != nil {
117-
return zero, fmt.Errorf("failed to login: %w", err)
118-
}
119-
// Apply backoff delay before retry
120-
backoff := calculateBackoff(retryCount + 1)
121-
if err := c.sleepFunc(ctx, backoff); err != nil {
122-
return zero, err
123-
}
124-
142+
shouldRetry, retryErr := handleRetryableError[T](ctx, c, err, retryCount)
143+
if retryErr != nil {
144+
return zero, retryErr
145+
}
146+
if shouldRetry {
125147
return genericRetry(ctx, c, method, uri, queryParams, bodyParams, needsKeys, needsAuth, retryCount+1, executeFunc)
126148
}
127149

@@ -178,70 +200,98 @@ func handleAPIResponse(response *APIBaseResponse) (string, error) {
178200
return "", NewAPIError("Request failed for an unknown reason")
179201
}
180202

181-
// executeAPIRequest handles the common logic for making API requests.
182-
// It returns the encrypted payload string on success, or an error.
183-
func (c *Client) executeAPIRequest(ctx context.Context, method, uri string, queryParams map[string]string, bodyParams map[string]any, needsAuth bool) (string, error) {
184-
timestamp := getTimestampStrMs()
203+
// preparedParams holds the prepared and encrypted request parameters.
204+
type preparedParams struct {
205+
originalQueryStr string
206+
encryptedQueryParams url.Values
207+
originalBodyStr string
208+
encryptedBody string
209+
}
210+
211+
// prepareRequestParams encrypts query and body parameters for an API request.
212+
func (c *Client) prepareRequestParams(queryParams map[string]string, bodyParams map[string]any) (preparedParams, error) {
213+
var params preparedParams
185214

186215
// Prepare query parameters (encrypted if provided)
187-
originalQueryStr := ""
188-
encryptedQueryParams := url.Values{}
189216
if len(queryParams) > 0 {
190217
queryValues := url.Values{}
191218
for k, v := range queryParams {
192219
queryValues.Add(k, v)
193220
}
194-
originalQueryStr = queryValues.Encode()
221+
params.originalQueryStr = queryValues.Encode()
195222

196-
encrypted, err := c.encryptPayloadUsingKey(originalQueryStr)
223+
encrypted, err := c.encryptPayloadUsingKey(params.originalQueryStr)
197224
if err != nil {
198-
return "", fmt.Errorf("failed to encrypt query params: %w", err)
225+
return params, fmt.Errorf("failed to encrypt query params: %w", err)
199226
}
200-
encryptedQueryParams.Add("params", encrypted)
227+
params.encryptedQueryParams = url.Values{}
228+
params.encryptedQueryParams.Add("params", encrypted)
201229
}
202230

203231
// Prepare body (encrypted if provided)
204-
originalBodyStr := ""
205-
encryptedBody := ""
206232
if len(bodyParams) > 0 {
207233
bodyJSON, err := json.Marshal(bodyParams)
208234
if err != nil {
209-
return "", fmt.Errorf("failed to marshal body params: %w", err)
235+
return params, fmt.Errorf("failed to marshal body params: %w", err)
210236
}
211-
originalBodyStr = string(bodyJSON)
237+
params.originalBodyStr = string(bodyJSON)
212238

213-
encrypted, err := c.encryptPayloadUsingKey(originalBodyStr)
239+
encrypted, err := c.encryptPayloadUsingKey(params.originalBodyStr)
214240
if err != nil {
215-
return "", fmt.Errorf("failed to encrypt body: %w", err)
241+
return params, fmt.Errorf("failed to encrypt body: %w", err)
216242
}
217-
encryptedBody = encrypted
243+
params.encryptedBody = encrypted
218244
}
219245

246+
return params, nil
247+
}
248+
249+
// calculateSignature determines the appropriate signature for the request.
250+
func (c *Client) calculateSignature(method, uri, originalQueryStr, originalBodyStr, timestamp string) string {
251+
switch {
252+
case uri == EndpointCheckVersion:
253+
return c.getSignFromTimestamp(timestamp)
254+
case method == http.MethodGet:
255+
return c.getSignFromPayloadAndTimestamp(originalQueryStr, timestamp)
256+
case method == http.MethodPost:
257+
return c.getSignFromPayloadAndTimestamp(originalBodyStr, timestamp)
258+
default:
259+
return ""
260+
}
261+
}
262+
263+
// buildHTTPRequest creates an HTTP request with all necessary headers.
264+
func (c *Client) buildHTTPRequest(ctx context.Context, method, uri, timestamp string, params preparedParams, needsAuth bool) (*http.Request, error) {
220265
// Build URL
221266
requestURL := c.baseURL + uri
222-
if len(encryptedQueryParams) > 0 {
223-
requestURL += "?" + encryptedQueryParams.Encode()
267+
if len(params.encryptedQueryParams) > 0 {
268+
requestURL += "?" + params.encryptedQueryParams.Encode()
224269
}
225270

226271
// Create request with context
227272
var req *http.Request
228273
var err error
229-
if encryptedBody != "" {
230-
req, err = http.NewRequestWithContext(ctx, method, requestURL, bytes.NewBufferString(encryptedBody))
274+
if params.encryptedBody != "" {
275+
req, err = http.NewRequestWithContext(ctx, method, requestURL, bytes.NewBufferString(params.encryptedBody))
231276
} else {
232277
req, err = http.NewRequestWithContext(ctx, method, requestURL, nil)
233278
}
234279
if err != nil {
235-
return "", fmt.Errorf("failed to create request: %w", err)
280+
return nil, fmt.Errorf("failed to create request: %w", err)
236281
}
237282

238283
// Generate sensor data
239284
sensorData, err := c.sensorDataBuilder.GenerateSensorData()
240285
if err != nil {
241-
return "", fmt.Errorf("failed to generate sensor data: %w", err)
286+
return nil, fmt.Errorf("failed to generate sensor data: %w", err)
242287
}
243288

244289
// Set headers
290+
accessToken := ""
291+
if needsAuth {
292+
accessToken = c.accessToken
293+
}
294+
245295
headers := map[string]string{
246296
"device-id": c.baseAPIDeviceID,
247297
"app-code": c.appCode,
@@ -253,29 +303,35 @@ func (c *Client) executeAPIRequest(ctx context.Context, method, uri string, quer
253303
"timestamp": timestamp,
254304
"Content-Type": "application/json",
255305
"X-acf-sensor-data": sensorData,
306+
"access-token": accessToken,
307+
"sign": c.calculateSignature(method, uri, params.originalQueryStr, params.originalBodyStr, timestamp),
256308
}
257309

258-
if needsAuth {
259-
headers["access-token"] = c.accessToken
260-
} else {
261-
headers["access-token"] = ""
310+
for k, v := range headers {
311+
req.Header.Set(k, v)
262312
}
263313

264-
// Calculate signature
265-
switch {
266-
case uri == EndpointCheckVersion:
267-
headers["sign"] = c.getSignFromTimestamp(timestamp)
268-
case method == http.MethodGet:
269-
headers["sign"] = c.getSignFromPayloadAndTimestamp(originalQueryStr, timestamp)
270-
case method == http.MethodPost:
271-
headers["sign"] = c.getSignFromPayloadAndTimestamp(originalBodyStr, timestamp)
272-
}
314+
c.logRequest(method, requestURL, headers, params.originalBodyStr)
273315

274-
for k, v := range headers {
275-
req.Header.Set(k, v)
316+
return req, nil
317+
}
318+
319+
// executeAPIRequest handles the common logic for making API requests.
320+
// It returns the encrypted payload string on success, or an error.
321+
func (c *Client) executeAPIRequest(ctx context.Context, method, uri string, queryParams map[string]string, bodyParams map[string]any, needsAuth bool) (string, error) {
322+
timestamp := getTimestampStrMs()
323+
324+
// Prepare and encrypt parameters
325+
params, err := c.prepareRequestParams(queryParams, bodyParams)
326+
if err != nil {
327+
return "", err
276328
}
277329

278-
c.logRequest(method, requestURL, headers, originalBodyStr)
330+
// Build HTTP request with headers
331+
req, err := c.buildHTTPRequest(ctx, method, uri, timestamp, params, needsAuth)
332+
if err != nil {
333+
return "", err
334+
}
279335

280336
// Send request
281337
resp, err := c.httpClient.Do(req)

internal/api/types.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,15 @@ type HVACInfo struct {
410410
TargetTempC float64
411411
}
412412

413+
// allDoorsLocked returns true if all doors are closed and locked.
414+
func allDoorsLocked(status DoorStatus) bool {
415+
return !status.DriverOpen && !status.PassengerOpen &&
416+
!status.RearLeftOpen && !status.RearRightOpen &&
417+
!status.TrunkOpen && !status.HoodOpen &&
418+
status.DriverLocked && status.PassengerLocked &&
419+
status.RearLeftLocked && status.RearRightLocked
420+
}
421+
413422
// GetDoorsInfo extracts door lock status from the vehicle status response.
414423
func (r *VehicleStatusResponse) GetDoorsInfo() (status DoorStatus, err error) {
415424
if len(r.AlertInfos) == 0 {
@@ -435,11 +444,7 @@ func (r *VehicleStatusResponse) GetDoorsInfo() (status DoorStatus, err error) {
435444
status.RearRightLocked = int(door.LockLinkSwRr) == DoorLocked
436445

437446
// All locked if no doors are open and all are locked
438-
status.AllLocked = !status.DriverOpen && !status.PassengerOpen &&
439-
!status.RearLeftOpen && !status.RearRightOpen &&
440-
!status.TrunkOpen && !status.HoodOpen &&
441-
status.DriverLocked && status.PassengerLocked &&
442-
status.RearLeftLocked && status.RearRightLocked
447+
status.AllLocked = allDoorsLocked(status)
443448

444449
return
445450
}

0 commit comments

Comments
 (0)