feat: add nightly branch workflow
This commit is contained in:
@@ -67,6 +67,7 @@ Based on the coverage report analysis, the following functions have gaps:
|
||||
```go
|
||||
func TestUserHandler_PreviewInviteURL_NonAdmin(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** User with "user" role
|
||||
- **Action:** POST /users/preview-invite-url
|
||||
- **Expected:** HTTP 403 Forbidden
|
||||
@@ -75,6 +76,7 @@ func TestUserHandler_PreviewInviteURL_NonAdmin(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_PreviewInviteURL_InvalidJSON(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user
|
||||
- **Action:** POST with invalid JSON body
|
||||
- **Expected:** HTTP 400 Bad Request
|
||||
@@ -83,6 +85,7 @@ func TestUserHandler_PreviewInviteURL_InvalidJSON(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_PreviewInviteURL_Success_Unconfigured(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user, no app.public_url setting
|
||||
- **Action:** POST with valid email
|
||||
- **Expected:** HTTP 200 OK
|
||||
@@ -95,6 +98,7 @@ func TestUserHandler_PreviewInviteURL_Success_Unconfigured(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_PreviewInviteURL_Success_Configured(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user, app.public_url setting exists
|
||||
- **Action:** POST with valid email
|
||||
- **Expected:** HTTP 200 OK
|
||||
@@ -105,6 +109,7 @@ func TestUserHandler_PreviewInviteURL_Success_Configured(t *testing.T)
|
||||
- `base_url` matches configured setting
|
||||
|
||||
**Mock Requirements:**
|
||||
|
||||
- Need to create Setting model with key "app.public_url"
|
||||
- Test both with and without configured URL
|
||||
|
||||
@@ -121,6 +126,7 @@ func TestUserHandler_PreviewInviteURL_Success_Configured(t *testing.T)
|
||||
```go
|
||||
func TestGetAppName_Default(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Empty database
|
||||
- **Action:** Call getAppName(db)
|
||||
- **Expected:** Returns "Charon"
|
||||
@@ -128,6 +134,7 @@ func TestGetAppName_Default(t *testing.T)
|
||||
```go
|
||||
func TestGetAppName_FromSettings(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Create Setting with key "app_name", value "MyCustomApp"
|
||||
- **Action:** Call getAppName(db)
|
||||
- **Expected:** Returns "MyCustomApp"
|
||||
@@ -135,11 +142,13 @@ func TestGetAppName_FromSettings(t *testing.T)
|
||||
```go
|
||||
func TestGetAppName_EmptyValue(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Create Setting with key "app_name", empty value
|
||||
- **Action:** Call getAppName(db)
|
||||
- **Expected:** Returns "Charon" (fallback)
|
||||
|
||||
**Mock Requirements:**
|
||||
|
||||
- Models.Setting with key "app_name"
|
||||
|
||||
---
|
||||
@@ -155,6 +164,7 @@ func TestGetAppName_EmptyValue(t *testing.T)
|
||||
```go
|
||||
func TestGenerateSecureToken_ReadError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Challenge:** `crypto/rand.Read()` rarely fails in normal conditions
|
||||
- **Approach:** This is difficult to test without mocking the rand.Reader
|
||||
- **Alternative:** Document that this error path is for catastrophic system failure
|
||||
@@ -173,6 +183,7 @@ func TestGenerateSecureToken_ReadError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_Setup_TransactionFailure(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Mock DB transaction failure
|
||||
- **Action:** POST /setup with valid data
|
||||
- **Challenge:** SQLite doesn't easily simulate transaction failures
|
||||
@@ -181,6 +192,7 @@ func TestUserHandler_Setup_TransactionFailure(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_Setup_PasswordHashError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Valid request but password hashing fails
|
||||
- **Challenge:** bcrypt.GenerateFromPassword rarely fails
|
||||
- **Decision:** May be acceptable uncovered code
|
||||
@@ -198,6 +210,7 @@ func TestUserHandler_Setup_PasswordHashError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_CreateUser_PasswordHashError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Valid request
|
||||
- **Action:** Attempt to create user with password that causes hash failure
|
||||
- **Challenge:** Hard to trigger without mocking
|
||||
@@ -206,6 +219,7 @@ func TestUserHandler_CreateUser_PasswordHashError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_CreateUser_DatabaseCheckError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Drop users table before email check
|
||||
- **Action:** POST /users
|
||||
- **Expected:** HTTP 500 "Failed to check email"
|
||||
@@ -213,6 +227,7 @@ func TestUserHandler_CreateUser_DatabaseCheckError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_CreateUser_AssociationError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Valid permitted_hosts with non-existent host IDs
|
||||
- **Action:** POST /users with invalid host IDs
|
||||
- **Expected:** Transaction should fail or hosts should be empty
|
||||
@@ -228,12 +243,14 @@ func TestUserHandler_CreateUser_AssociationError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_InviteUser_TokenGenerationError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Challenge:** Hard to force crypto/rand failure
|
||||
- **Decision:** Document as edge case
|
||||
|
||||
```go
|
||||
func TestUserHandler_InviteUser_DisableUserError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Create user, then cause Update to fail
|
||||
- **Action:** POST /users/invite
|
||||
- **Expected:** Transaction rollback
|
||||
@@ -241,6 +258,7 @@ func TestUserHandler_InviteUser_DisableUserError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_InviteUser_MailServiceConfigured(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Configure MailService with valid SMTP settings
|
||||
- **Action:** POST /users/invite
|
||||
- **Expected:** email_sent should be true (or handle SMTP error)
|
||||
@@ -260,6 +278,7 @@ func TestUserHandler_InviteUser_MailServiceConfigured(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_UpdateUser_EmailConflict(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Create two users
|
||||
- **Action:** Try to update user1's email to user2's email
|
||||
- **Expected:** HTTP 409 Conflict
|
||||
@@ -276,6 +295,7 @@ func TestUserHandler_UpdateUser_EmailConflict(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_UpdateProfile_EmailCheckError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Valid user, drop table before email check
|
||||
- **Action:** PUT /profile with new email
|
||||
- **Expected:** HTTP 500 "Failed to check email availability"
|
||||
@@ -283,6 +303,7 @@ func TestUserHandler_UpdateProfile_EmailCheckError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_UpdateProfile_UpdateError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Valid user, close DB before update
|
||||
- **Action:** PUT /profile
|
||||
- **Expected:** HTTP 500 "Failed to update profile"
|
||||
@@ -294,6 +315,7 @@ func TestUserHandler_UpdateProfile_UpdateError(t *testing.T)
|
||||
**Current Coverage:** 81.8%
|
||||
|
||||
**Existing Tests Cover:**
|
||||
|
||||
- Invalid JSON
|
||||
- Invalid token
|
||||
- Expired token (with status update)
|
||||
@@ -307,6 +329,7 @@ func TestUserHandler_UpdateProfile_UpdateError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_AcceptInvite_PasswordHashError(t *testing.T)
|
||||
```
|
||||
|
||||
- **Challenge:** Hard to trigger bcrypt failure
|
||||
- **Decision:** Document as edge case
|
||||
|
||||
@@ -321,13 +344,15 @@ func TestUserHandler_AcceptInvite_PasswordHashError(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_CreateUser_EmailNormalization(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user
|
||||
- **Action:** Create user with email "User@Example.COM"
|
||||
- **Expected:** Email stored as "user@example.com"
|
||||
- **Action:** Create user with email "<User@Example.COM>"
|
||||
- **Expected:** Email stored as "<user@example.com>"
|
||||
|
||||
```go
|
||||
func TestUserHandler_InviteUser_EmailNormalization(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user
|
||||
- **Action:** Invite user with mixed-case email
|
||||
- **Expected:** Email stored lowercase
|
||||
@@ -341,6 +366,7 @@ func TestUserHandler_InviteUser_EmailNormalization(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_CreateUser_DefaultPermissionMode(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user
|
||||
- **Action:** Create user without specifying permission_mode
|
||||
- **Expected:** permission_mode defaults to "allow_all"
|
||||
@@ -348,6 +374,7 @@ func TestUserHandler_CreateUser_DefaultPermissionMode(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_InviteUser_DefaultPermissionMode(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user
|
||||
- **Action:** Invite user without specifying permission_mode
|
||||
- **Expected:** permission_mode defaults to "allow_all"
|
||||
@@ -361,6 +388,7 @@ func TestUserHandler_InviteUser_DefaultPermissionMode(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_CreateUser_DefaultRole(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user
|
||||
- **Action:** Create user without specifying role
|
||||
- **Expected:** role defaults to "user"
|
||||
@@ -368,6 +396,7 @@ func TestUserHandler_CreateUser_DefaultRole(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_InviteUser_DefaultRole(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin user
|
||||
- **Action:** Invite user without specifying role
|
||||
- **Expected:** role defaults to "user"
|
||||
@@ -383,6 +412,7 @@ func TestUserHandler_InviteUser_DefaultRole(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_CreateUser_EmptyPermittedHosts(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin, permission_mode "deny_all", empty permitted_hosts
|
||||
- **Action:** Create user
|
||||
- **Expected:** User created with deny_all mode, no permitted hosts
|
||||
@@ -390,6 +420,7 @@ func TestUserHandler_CreateUser_EmptyPermittedHosts(t *testing.T)
|
||||
```go
|
||||
func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
|
||||
```
|
||||
|
||||
- **Setup:** Admin, permission_mode "deny_all", non-existent host IDs [999, 1000]
|
||||
- **Action:** Create user
|
||||
- **Expected:** User created but no hosts associated (or error)
|
||||
@@ -399,6 +430,7 @@ func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Add Missing Tests (Priority 1)
|
||||
|
||||
1. Implement PreviewInviteURL test suite (4 tests)
|
||||
2. Implement getAppName test suite (3 tests)
|
||||
3. Run coverage and verify these reach 100%
|
||||
@@ -406,6 +438,7 @@ func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
|
||||
**Expected Impact:** +7 test cases, ~35 lines of untested code covered
|
||||
|
||||
### Phase 2: Error Path Coverage (Priority 2)
|
||||
|
||||
1. Add database error simulation tests where feasible
|
||||
2. Document hard-to-test error paths with code comments
|
||||
3. Focus on testable scenarios (table drops, closed connections)
|
||||
@@ -413,6 +446,7 @@ func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
|
||||
**Expected Impact:** +8-10 test cases, improved error path coverage
|
||||
|
||||
### Phase 3: Edge Cases and Defaults (Priority 3)
|
||||
|
||||
1. Add email normalization tests
|
||||
2. Add default value tests
|
||||
3. Verify role and permission defaults
|
||||
@@ -420,6 +454,7 @@ func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
|
||||
**Expected Impact:** +6 test cases, better validation of business logic
|
||||
|
||||
### Phase 4: Integration Edge Cases (Priority 4)
|
||||
|
||||
1. Add permitted hosts edge case tests
|
||||
2. Test association behavior with invalid data
|
||||
|
||||
@@ -430,6 +465,7 @@ func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
|
||||
## Success Criteria
|
||||
|
||||
### Minimum Requirements (85% Coverage)
|
||||
|
||||
- [ ] PreviewInviteURL: 100% coverage (4 tests)
|
||||
- [ ] getAppName: 100% coverage (3 tests)
|
||||
- [ ] generateSecureToken: 100% or documented as untestable
|
||||
@@ -437,6 +473,7 @@ func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
|
||||
- [ ] Total user_handler.go coverage: ≥85%
|
||||
|
||||
### Stretch Goal (100% Coverage)
|
||||
|
||||
- [ ] All testable code paths covered
|
||||
- [ ] Untestable code paths documented with `// Coverage: Untestable without mocking` comments
|
||||
- [ ] All error paths tested or documented
|
||||
@@ -447,6 +484,7 @@ func TestUserHandler_CreateUser_NonExistentPermittedHosts(t *testing.T)
|
||||
## Test Execution Plan
|
||||
|
||||
### Step 1: Run Baseline Coverage
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go test -coverprofile=baseline_coverage.txt -run "TestUserHandler" ./internal/api/handlers
|
||||
@@ -454,16 +492,19 @@ go tool cover -func=baseline_coverage.txt | grep user_handler.go
|
||||
```
|
||||
|
||||
### Step 2: Implement Priority 1 Tests
|
||||
|
||||
- Add PreviewInviteURL tests
|
||||
- Add getAppName tests
|
||||
- Run coverage and verify improvement
|
||||
|
||||
### Step 3: Iterate Through Priorities
|
||||
|
||||
- Implement each priority group
|
||||
- Run coverage after each group
|
||||
- Adjust plan based on results
|
||||
|
||||
### Step 4: Final Coverage Report
|
||||
|
||||
```bash
|
||||
go test -coverprofile=final_coverage.txt -run "TestUserHandler" ./internal/api/handlers
|
||||
go tool cover -func=final_coverage.txt | grep user_handler.go
|
||||
@@ -471,6 +512,7 @@ go tool cover -html=final_coverage.txt -o user_handler_coverage.html
|
||||
```
|
||||
|
||||
### Step 5: Validate Against Codecov
|
||||
|
||||
- Push changes to branch
|
||||
- Verify Codecov report shows ≥85% patch coverage
|
||||
- Verify no coverage regressions
|
||||
@@ -480,17 +522,20 @@ go tool cover -html=final_coverage.txt -o user_handler_coverage.html
|
||||
## Mock and Setup Requirements
|
||||
|
||||
### Database Models
|
||||
|
||||
- `models.User`
|
||||
- `models.Setting`
|
||||
- `models.ProxyHost`
|
||||
|
||||
### Test Helpers
|
||||
|
||||
- `setupUserHandler(t)` - Creates test DB with User and Setting tables
|
||||
- `setupUserHandlerWithProxyHosts(t)` - Includes ProxyHost table
|
||||
- Admin middleware mock: `c.Set("role", "admin")`
|
||||
- User ID middleware mock: `c.Set("userID", uint(1))`
|
||||
|
||||
### Additional Mocks Needed
|
||||
|
||||
- SMTP server mock for email testing (optional, can verify email_sent=false)
|
||||
- Settings helper for creating app.public_url and app_name settings
|
||||
|
||||
@@ -511,6 +556,7 @@ go tool cover -html=final_coverage.txt -o user_handler_coverage.html
|
||||
## Code Style Consistency
|
||||
|
||||
### Existing Patterns to Maintain
|
||||
|
||||
- Use `gin.SetMode(gin.TestMode)` at test start
|
||||
- Use `httptest.NewRecorder()` for response capture
|
||||
- Marshal request bodies with `json.Marshal()`
|
||||
@@ -519,6 +565,7 @@ go tool cover -html=final_coverage.txt -o user_handler_coverage.html
|
||||
- Use `assert.Equal()` for assertions
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Group related tests with `t.Run()` when appropriate
|
||||
- Keep tests in same file as existing tests
|
||||
- Use clear comments for complex setup
|
||||
@@ -542,17 +589,20 @@ go tool cover -html=final_coverage.txt -o user_handler_coverage.html
|
||||
## Notes and Considerations
|
||||
|
||||
### Hard-to-Test Scenarios
|
||||
|
||||
1. **crypto/rand.Read() failure:** Extremely rare, requires system-level failure
|
||||
2. **bcrypt password hashing failure:** Rare, usually only with invalid cost
|
||||
3. **SMTP email sending:** Requires mock server or test credentials
|
||||
|
||||
### Recommendations
|
||||
|
||||
- Document untestable error paths with comments
|
||||
- Focus test effort on realistic failure scenarios
|
||||
- Use table drops and closed connections for DB errors
|
||||
- Consider refactoring hard-to-test code if coverage is critical
|
||||
|
||||
### Future Improvements
|
||||
|
||||
- Consider dependency injection for crypto/rand and bcrypt
|
||||
- Add integration tests with real SMTP mock server
|
||||
- Add performance tests for password hashing
|
||||
|
||||
Reference in New Issue
Block a user