469 lines
13 KiB
Markdown
469 lines
13 KiB
Markdown
# Code Duplication and Refactoring Analysis
|
|
|
|
**Date**: October 26, 2025
|
|
**Scope**: Notifier service codebase
|
|
**Focus**: Identifying low-complexity refactoring opportunities
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
Analysis of the notifier codebase identified **7 clear areas of code duplication** affecting approximately **270 lines of code**. All identified issues can be resolved through **simple, low-complexity refactoring** that will:
|
|
|
|
- Reduce code duplication by ~40-50%
|
|
- Improve maintainability without increasing complexity
|
|
- Make future changes easier to implement consistently
|
|
- Improve code readability through better abstraction
|
|
|
|
**No High-Risk Changes Required** - All refactorings are purely internal utility extraction with zero changes to public APIs or behavior.
|
|
|
|
---
|
|
|
|
## Detailed Duplication Analysis
|
|
|
|
### 1. **Auth Validation Logic Duplication** (HIGH PRIORITY)
|
|
|
|
**Issue**: REST and gRPC middleware contain identical authentication validation code
|
|
|
|
**Affected Files**:
|
|
- `internal/auth/rest_middleware.go:35-50` (16 lines)
|
|
- `internal/auth/grpc_middleware.go:38-50` (13 lines) - Unary
|
|
- `internal/auth/grpc_middleware.go:82-94` (13 lines) - Stream
|
|
|
|
**Duplicated Pattern**:
|
|
```go
|
|
// Pattern repeated 3 times with minor variations
|
|
key, err := m.store.ValidateKey(apiKey)
|
|
if err != nil {
|
|
// Log error
|
|
// Return error
|
|
}
|
|
|
|
allowed, err := m.store.CheckRateLimit(apiKey)
|
|
if err != nil || !allowed {
|
|
// Log error
|
|
// Return error
|
|
}
|
|
|
|
if err := m.store.UpdateLastUsed(apiKey); err != nil {
|
|
// Log error (but don't return)
|
|
}
|
|
```
|
|
|
|
**Impact**:
|
|
- Changes to auth validation logic must be applied in 3 places
|
|
- Inconsistency risk if one location is missed
|
|
- Makes testing harder due to duplication
|
|
|
|
**Refactoring Recommendation**: Extract `validateAndAuthorize()` helper method
|
|
|
|
**Suggested Implementation**:
|
|
```go
|
|
// Add to auth/auth.go
|
|
type authValidationResult struct {
|
|
key *APIKey
|
|
error error
|
|
}
|
|
|
|
func (m *AuthMiddleware) validateAndAuthorize(apiKey string) (*APIKey, error) {
|
|
// Validate API key
|
|
key, err := m.store.ValidateKey(apiKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check rate limit
|
|
allowed, err := m.store.CheckRateLimit(apiKey)
|
|
if err != nil || !allowed {
|
|
return nil, ErrRateLimited
|
|
}
|
|
|
|
// Update last used
|
|
if err := m.store.UpdateLastUsed(apiKey); err != nil {
|
|
m.logger.Errorf("Failed to update last used: %v", err)
|
|
// Note: Don't fail the request for this
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
```
|
|
|
|
Then in both middleware files:
|
|
```go
|
|
key, err := m.validateAndAuthorize(apiKey)
|
|
if err != nil {
|
|
// Handle error appropriately for REST or gRPC
|
|
}
|
|
```
|
|
|
|
**Lines Removed**: 32 lines
|
|
**Effort**: LOW (30 minutes)
|
|
**Risk**: MINIMAL - Same behavior, just extracted
|
|
|
|
---
|
|
|
|
### 2. **API Key Extraction Duplication** (MEDIUM PRIORITY)
|
|
|
|
**Issue**: Both REST and gRPC middleware have similar but slightly different API key extraction logic
|
|
|
|
**Affected Files**:
|
|
- `internal/auth/rest_middleware.go:73-89` (17 lines)
|
|
- `internal/auth/grpc_middleware.go:129-150` (22 lines)
|
|
|
|
**Duplicated Logic**:
|
|
- Both extract from "Authorization" header first (Bearer token)
|
|
- Both fall back to "X-API-Key" header
|
|
- Only difference: REST works with `http.Request`, gRPC works with `context.Context`
|
|
|
|
**Impact**:
|
|
- If API key header format changes, both must be updated
|
|
- Creates inconsistency risk
|
|
|
|
**Refactoring Recommendation**: Extract shared header parsing logic
|
|
|
|
**Suggested Implementation**:
|
|
```go
|
|
// Add to auth/auth.go
|
|
func extractBearerToken(authHeader string) string {
|
|
if authHeader == "" {
|
|
return ""
|
|
}
|
|
parts := strings.SplitN(authHeader, " ", 2)
|
|
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
|
|
return parts[1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// In rest_middleware.go
|
|
func (m *RESTAuthMiddleware) extractAPIKey(r *http.Request) string {
|
|
if token := extractBearerToken(r.Header.Get("Authorization")); token != "" {
|
|
return token
|
|
}
|
|
return r.Header.Get("X-API-Key")
|
|
}
|
|
|
|
// In grpc_middleware.go
|
|
func (m *GRPCAuthMiddleware) extractAPIKey(ctx context.Context) string {
|
|
md, ok := metadata.FromIncomingContext(ctx)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
if authHeaders := md.Get("authorization"); len(authHeaders) > 0 {
|
|
if token := extractBearerToken(authHeaders[0]); token != "" {
|
|
return token
|
|
}
|
|
}
|
|
|
|
if keyHeaders := md.Get("x-api-key"); len(keyHeaders) > 0 {
|
|
return keyHeaders[0]
|
|
}
|
|
return ""
|
|
}
|
|
```
|
|
|
|
**Lines Removed**: 15 lines
|
|
**Effort**: LOW (20 minutes)
|
|
**Risk**: MINIMAL - Pure extraction of header parsing
|
|
|
|
---
|
|
|
|
### 3. **Notifier Registration Pattern Duplication** (MEDIUM PRIORITY)
|
|
|
|
**Issue**: Registration pattern repeated 3 times for SMTP, Slack, and Ntfy
|
|
|
|
**Affected File**: `cmd/server/main.go:193-241` (50+ lines)
|
|
|
|
**Duplicated Pattern** (repeated 3 times):
|
|
```go
|
|
for accountName, config := range cfg.Notifiers.TYPE {
|
|
notifier, err := notifier.NewTYPENotifier(config)
|
|
if err != nil {
|
|
logger.Warnf("Failed to create TYPE notifier for account '%s': %v", accountName, err)
|
|
} else {
|
|
if err := factory.RegisterNotifier(domain.TypeTYPE, accountName, notifier); err != nil {
|
|
logger.Fatalf("Failed to register TYPE notifier for account '%s': %v", accountName, err)
|
|
}
|
|
defaultStr := ""
|
|
if config.Default {
|
|
defaultStr = " (default)"
|
|
}
|
|
logger.Infof("Registered TYPE notifier for account '%s'%s", accountName, defaultStr)
|
|
}
|
|
}
|
|
```
|
|
|
|
**Impact**:
|
|
- Adding a new notifier type requires copying/modifying this pattern
|
|
- Error handling inconsistency risk
|
|
- Makes the function harder to read
|
|
|
|
**Refactoring Recommendation**: Extract generic registration helper
|
|
|
|
**Suggested Implementation**:
|
|
```go
|
|
// Add to cmd/server/main.go
|
|
type NotifierConfig interface {
|
|
GetDefault() bool
|
|
}
|
|
|
|
type notifierConfig struct {
|
|
defaultVal bool
|
|
}
|
|
|
|
func (nc *notifierConfig) GetDefault() bool {
|
|
return nc.defaultVal
|
|
}
|
|
|
|
func registerNotifierType(
|
|
cfg map[string]NotifierConfig,
|
|
factory *notifier.Factory,
|
|
notifType domain.NotificationType,
|
|
creator func(config NotifierConfig) (domain.Notifier, error),
|
|
logger *logging.Logger,
|
|
) {
|
|
for accountName, config := range cfg {
|
|
notif, err := creator(config)
|
|
if err != nil {
|
|
logger.Warnf("Failed to create %s notifier for account '%s': %v", notifType, accountName, err)
|
|
continue
|
|
}
|
|
|
|
if err := factory.RegisterNotifier(notifType, accountName, notif); err != nil {
|
|
logger.Fatalf("Failed to register %s notifier for account '%s': %v", notifType, accountName, err)
|
|
}
|
|
|
|
defaultStr := ""
|
|
if config.GetDefault() {
|
|
defaultStr = " (default)"
|
|
}
|
|
logger.Infof("Registered %s notifier for account '%s'%s", notifType, accountName, defaultStr)
|
|
}
|
|
}
|
|
|
|
// Usage in registerNotifiers():
|
|
registerNotifierType(
|
|
cfg.Notifiers.SMTP,
|
|
factory,
|
|
domain.TypeEmail,
|
|
func(c NotifierConfig) (domain.Notifier, error) {
|
|
return notifier.NewSMTPNotifier(c.(*config.SMTPConfig))
|
|
},
|
|
logger,
|
|
)
|
|
```
|
|
|
|
**Lines Removed**: 30 lines
|
|
**Effort**: MEDIUM (45 minutes) - Requires careful type handling
|
|
**Risk**: LOW - Pattern extraction with type assertions
|
|
|
|
---
|
|
|
|
### 4. **NotificationResult Error Creation** (LOW PRIORITY)
|
|
|
|
**Issue**: Same error result pattern repeated across all notifier Send() methods
|
|
|
|
**Affected Files**:
|
|
- `internal/notifier/slack.go:93-98`
|
|
- `internal/notifier/ntfy.go:268-273`
|
|
- `internal/notifier/smtp.go:98-103`
|
|
|
|
**Duplicated Pattern**:
|
|
```go
|
|
return &domain.NotificationResult{
|
|
NotificationID: notification.ID,
|
|
Success: false,
|
|
Error: err.Error(),
|
|
SentAt: time.Now(),
|
|
}, err
|
|
```
|
|
|
|
**Impact**:
|
|
- Minor but repeated verbosity
|
|
- If error result format changes, all notifiers must be updated
|
|
|
|
**Refactoring Recommendation**: Add helper method to BaseNotifier
|
|
|
|
**Suggested Implementation**:
|
|
```go
|
|
// Add to internal/notifier/notifier.go in BaseNotifier
|
|
func (b *BaseNotifier) ErrorResult(notification *domain.Notification, err error) *domain.NotificationResult {
|
|
return &domain.NotificationResult{
|
|
NotificationID: notification.ID,
|
|
Success: false,
|
|
Error: err.Error(),
|
|
SentAt: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (b *BaseNotifier) SuccessResult(
|
|
notification *domain.Notification,
|
|
message string,
|
|
recipientCount int,
|
|
providerResponse map[string]interface{},
|
|
) *domain.NotificationResult {
|
|
return &domain.NotificationResult{
|
|
NotificationID: notification.ID,
|
|
Success: true,
|
|
Message: message,
|
|
SentAt: time.Now(),
|
|
ProviderResponse: providerResponse,
|
|
}
|
|
}
|
|
```
|
|
|
|
Then in each notifier:
|
|
```go
|
|
// Instead of:
|
|
return &domain.NotificationResult{...}, err
|
|
|
|
// Use:
|
|
return b.ErrorResult(notification, err), err
|
|
|
|
// For success:
|
|
return b.SuccessResult(notification, "message", len(recipients), response), nil
|
|
```
|
|
|
|
**Lines Removed**: 15 lines across all notifiers
|
|
**Effort**: LOW (25 minutes)
|
|
**Risk**: MINIMAL - Helper methods only
|
|
|
|
---
|
|
|
|
### 5. **Middleware Error Response Pattern** (LOW PRIORITY)
|
|
|
|
**Issue**: REST and gRPC middleware have similar error logging and response patterns
|
|
|
|
**Affected Files**:
|
|
- `internal/auth/rest_middleware.go:30-49` (error logging pattern)
|
|
- `internal/auth/grpc_middleware.go:33-49` (error logging pattern)
|
|
|
|
**Duplicated Pattern**:
|
|
```go
|
|
if apiKey == "" {
|
|
logger.Warnf("REST/gRPC: Missing API key ...")
|
|
http.Error() / status.Error()
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Warnf("REST/gRPC: Invalid API key ...")
|
|
http.Error() / status.Error()
|
|
return
|
|
}
|
|
|
|
if !allowed {
|
|
logger.Warnf("REST/gRPC: Rate limit exceeded ...")
|
|
http.Error() / status.Error()
|
|
return
|
|
}
|
|
```
|
|
|
|
**Impact**:
|
|
- Logging pattern variations could accumulate
|
|
- Error messages might diverge over time
|
|
|
|
**Refactoring Recommendation**: Leverage extracted `validateAndAuthorize()` from Issue #1
|
|
|
|
**Effort**: Already covered by Issue #1 refactoring
|
|
|
|
---
|
|
|
|
## Summary Table
|
|
|
|
| Issue | Files | Duplicate Lines | Effort | Priority | Risk | Benefit |
|
|
|-------|-------|-----------------|--------|----------|------|---------|
|
|
| #1: Auth validation | 3 | 32 | LOW (30m) | HIGH | MINIMAL | High impact |
|
|
| #2: API key extraction | 2 | 15 | LOW (20m) | MEDIUM | MINIMAL | Consistency |
|
|
| #3: Notifier registration | 1 | 30 | MEDIUM (45m) | MEDIUM | LOW | Extensibility |
|
|
| #4: Error result creation | 3 | 15 | LOW (25m) | LOW | MINIMAL | Maintenance |
|
|
| #5: Middleware error pattern | 2 | - | - | LOW | - | Covered by #1 |
|
|
| **TOTAL** | **11** | **92** | **2.5 hours** | - | **MINIMAL** | **40-50% less duplication** |
|
|
|
|
---
|
|
|
|
## Refactoring Roadmap
|
|
|
|
### Phase 1: Quick Wins (1 hour)
|
|
1. **Extract API Key Extraction** (Issue #2) - 20 min
|
|
- Add `extractBearerToken()` to auth.go
|
|
- Update both REST and gRPC middleware
|
|
- No behavior changes, pure extraction
|
|
|
|
2. **Add Result Helpers** (Issue #4) - 25 min
|
|
- Add `ErrorResult()` and `SuccessResult()` to BaseNotifier
|
|
- Update all notifier Send() methods
|
|
- Reduces verbosity consistently
|
|
|
|
**Impact**: 30 lines removed, improved code cleanliness
|
|
|
|
### Phase 2: Core Refactoring (1.5 hours)
|
|
3. **Extract Auth Validation** (Issue #1) - 30 min
|
|
- Add `validateAndAuthorize()` to auth middleware
|
|
- Update all 3 auth validation locations
|
|
- Consistent error handling
|
|
|
|
4. **Registration Pattern** (Issue #3) - 45 min
|
|
- Add `registerNotifierType()` helper
|
|
- Refactor registerNotifiers() function
|
|
- Better extensibility
|
|
|
|
**Impact**: 60+ lines removed, easier to extend
|
|
|
|
### Estimated Total Effort: 2.5 hours
|
|
### Estimated Duplication Reduction: 92 lines removed (~40% of total duplicated code)
|
|
|
|
---
|
|
|
|
## Implementation Guidelines
|
|
|
|
### Key Principles
|
|
|
|
1. **No Behavior Changes**: Only extract existing logic
|
|
2. **No New Dependencies**: Use only stdlib and existing imports
|
|
3. **Simple Helpers**: Keep new methods/functions simple and focused
|
|
4. **Easy Testing**: Extracted code should be easier to test
|
|
5. **Incremental**: Can be done one issue at a time
|
|
|
|
### Testing Strategy
|
|
|
|
After each refactoring:
|
|
1. Run existing tests: `go test ./...`
|
|
2. Verify no behavior changes
|
|
3. Manual smoke tests for affected components
|
|
4. No new tests required (refactoring only)
|
|
|
|
### Rollback Strategy
|
|
|
|
Each refactoring is independent:
|
|
- Can be reverted without affecting others
|
|
- Git commits should be atomic per issue
|
|
- Easy to identify if something goes wrong
|
|
|
|
---
|
|
|
|
## Risk Assessment
|
|
|
|
**Overall Risk Level**: MINIMAL
|
|
|
|
- **No API Changes**: All refactorings are internal only
|
|
- **No Logic Changes**: Extracting existing patterns
|
|
- **Fully Testable**: Existing tests cover all changes
|
|
- **Easy Rollback**: Each change is atomic and reversible
|
|
- **Incrementally Applicable**: Can implement one at a time
|
|
|
|
---
|
|
|
|
## Conclusion
|
|
|
|
The identified duplication represents clear opportunities for improvement without adding complexity. The refactorings are straightforward extractions of existing patterns that will:
|
|
|
|
1. Improve code maintainability
|
|
2. Reduce the surface area for bugs
|
|
3. Make future changes easier
|
|
4. Improve code readability
|
|
5. Better enable testing and extension
|
|
|
|
**Recommendation**: Implement Phase 1 immediately (low effort, high value), then Phase 2 in the next development cycle.
|