docs(dashboard): add comprehensive unit and integration test phases
Phase 14: Unit Tests (2-3 hours) - Service layer tests (dashboard_service_test.go) - filterHiddenSections() - tests no filters, one hidden, multiple hidden - reorderSections() - tests default order, custom order, partial order - Handler helper tests (dashboard_test.go) - getSectionType() - smart vs collection sections - getSectionTitle() - all smart sections and collections - getSectionIcon() - icons for all sections - getSectionViewAllURL() - URLs for all sections - Table-driven tests for multiple scenarios - Uses testify/assert - Skips database-dependent tests (use integration tests instead) Phase 15: Integration Tests (2-3 hours) - File: cmd/server/tests/dashboard_test.go - Uses setupTestServer(t) helper from test_helpers.go - Tests /api/dashboard/sections JSON endpoint: - Three-context testing (no auth, user, admin) - Missing library_id → 400 - Invalid library_id → 400 - With limit parameter - Tests user preferences: - Hidden sections filtered correctly - Custom order applied correctly - Tests SSR /dashboard page: - Returns HTML with dashboard elements - Requires auth - Helper functions: - updateDashboardPreferences() - getUserUUIDFromToken() - parseUUID() Testing Strategy: - Unit tests alongside source files (project convention) - Integration tests in cmd/server/tests/ (project convention) - setupTestServer() helper creates full test environment - Uses loginTestUser(), loginRegularUser(), setupDeviceTest() - Follows existing patterns from auth_test.go, collections_bulk_test.go Updated Timeline: 23-31 days total (added 4-6 hours for testing) Benefits: - Comprehensive test coverage before production - Catches regressions in user preferences logic - Validates API endpoint behavior across contexts - Ensures SSR and JSON return consistent data - Follows project testing conventions
This commit is contained in:
+530
-1
@@ -1932,6 +1932,513 @@ bru run bruno/dashboard/ --env local
|
||||
|
||||
---
|
||||
|
||||
### **Phase 14: Unit Tests** (2-3 hours)
|
||||
|
||||
**COMPLIANCE**: Unit tests alongside source files, following project patterns
|
||||
|
||||
#### 14.1 Service Layer Unit Tests
|
||||
**File: `internal/services/dashboard_service_test.go`** (new file)
|
||||
|
||||
```go
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"bookhoard/internal/database"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFilterHiddenSections(t *testing.T) {
|
||||
service := &DashboardService{}
|
||||
|
||||
items := []SectionItems{
|
||||
{SectionKey: "continue-reading", Items: nil},
|
||||
{SectionKey: "in-progress", Items: nil},
|
||||
{SectionKey: "recently-added", Items: nil},
|
||||
}
|
||||
|
||||
t.Run("No hidden sections", func(t *testing.T) {
|
||||
result := service.filterHiddenSections(items, []string{})
|
||||
assert.Equal(t, 3, len(result))
|
||||
})
|
||||
|
||||
t.Run("Hide one section", func(t *testing.T) {
|
||||
result := service.filterHiddenSections(items, []string{"in-progress"})
|
||||
assert.Equal(t, 2, len(result))
|
||||
assert.Equal(t, "continue-reading", result[0].SectionKey)
|
||||
assert.Equal(t, "recently-added", result[1].SectionKey)
|
||||
})
|
||||
|
||||
t.Run("Hide multiple sections", func(t *testing.T) {
|
||||
result := service.filterHiddenSections(items, []string{"continue-reading", "recently-added"})
|
||||
assert.Equal(t, 1, len(result))
|
||||
assert.Equal(t, "in-progress", result[0].SectionKey)
|
||||
})
|
||||
}
|
||||
|
||||
func TestReorderSections(t *testing.T) {
|
||||
service := &DashboardService{}
|
||||
|
||||
items := []SectionItems{
|
||||
{SectionKey: "continue-reading", Items: nil},
|
||||
{SectionKey: "in-progress", Items: nil},
|
||||
{SectionKey: "recently-added", Items: nil},
|
||||
}
|
||||
|
||||
t.Run("No custom order", func(t *testing.T) {
|
||||
result := service.reorderSections(items, []string{})
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "continue-reading", result[0].SectionKey)
|
||||
})
|
||||
|
||||
t.Run("Custom order - all sections", func(t *testing.T) {
|
||||
customOrder := []string{"recently-added", "continue-reading", "in-progress"}
|
||||
result := service.reorderSections(items, customOrder)
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "recently-added", result[0].SectionKey)
|
||||
assert.Equal(t, "continue-reading", result[1].SectionKey)
|
||||
assert.Equal(t, "in-progress", result[2].SectionKey)
|
||||
})
|
||||
|
||||
t.Run("Custom order - partial (new sections appended)", func(t *testing.T) {
|
||||
customOrder := []string{"in-progress", "continue-reading"}
|
||||
result := service.reorderSections(items, customOrder)
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "in-progress", result[0].SectionKey)
|
||||
assert.Equal(t, "continue-reading", result[1].SectionKey)
|
||||
assert.Equal(t, "recently-added", result[2].SectionKey) // Appended at end
|
||||
})
|
||||
|
||||
t.Run("Custom order - unknown section ignored", func(t *testing.T) {
|
||||
customOrder := []string{"unknown-section", "continue-reading"}
|
||||
result := service.reorderSections(items, customOrder)
|
||||
assert.Equal(t, 3, len(result))
|
||||
assert.Equal(t, "continue-reading", result[0].SectionKey)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDashboardPreferences(t *testing.T) {
|
||||
// This would require a test database setup
|
||||
// For now, test with mock or skip
|
||||
t.Skip("Requires database integration - use integration tests")
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ Unit tests alongside source file (`dashboard_service_test.go`)
|
||||
- ✅ Test pure functions (filterHiddenSections, reorderSections)
|
||||
- ✅ Table-driven tests for multiple scenarios
|
||||
- ✅ Use testify/assert for assertions
|
||||
- ✅ Skip database-dependent tests (use integration tests)
|
||||
|
||||
#### 14.2 Handler Unit Tests
|
||||
**File: `internal/handlers/dashboard_test.go`** (new file)
|
||||
|
||||
```go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetSectionType(t *testing.T) {
|
||||
t.Run("Smart sections", func(t *testing.T) {
|
||||
smartSections := []string{
|
||||
"continue-reading", "in-progress", "recently-added",
|
||||
"recently-read", "unread",
|
||||
}
|
||||
for _, key := range smartSections {
|
||||
result := getSectionType(key)
|
||||
assert.Equal(t, "smart", result, "Section %s should be smart", key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Collection sections", func(t *testing.T) {
|
||||
result := getSectionType("collection-uuid-123")
|
||||
assert.Equal(t, "collection", result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetSectionTitle(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{"continue-reading", "Continue Reading"},
|
||||
{"in-progress", "In Progress"},
|
||||
{"recently-added", "Recently Added"},
|
||||
{"recently-read", "Recently Read"},
|
||||
{"unread", "Not Started"},
|
||||
{"my-custom-collection", "my-custom-collection"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
result := getSectionTitle(tt.key)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSectionIcon(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{"continue-reading", "📖"},
|
||||
{"in-progress", "📚"},
|
||||
{"recently-added", "🆕"},
|
||||
{"recently-read", "✅"},
|
||||
{"unread", "📕"},
|
||||
{"unknown", "📚"}, // Default
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
result := getSectionIcon(tt.key)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSectionViewAllURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{"continue-reading", "/section/continue-reading"},
|
||||
{"in-progress", "/section/in-progress"},
|
||||
{"recently-added", "/section/recently-added"},
|
||||
{"recently-read", "/history"},
|
||||
{"unread", "/section/unread"},
|
||||
{"my-collection", ""}, // Collections don't have view-all
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.key, func(t *testing.T) {
|
||||
result := getSectionViewAllURL(tt.key)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ Unit tests alongside handler file (`dashboard_test.go`)
|
||||
- ✅ Test pure helper functions (getSectionType, getSectionTitle, etc.)
|
||||
- ✅ Table-driven tests for multiple scenarios
|
||||
- ✅ No HTTP requests (use integration tests)
|
||||
|
||||
---
|
||||
|
||||
### **Phase 15: Integration Tests** (2-3 hours)
|
||||
|
||||
**COMPLIANCE**: Integration tests in `cmd/server/tests/`, using `setupTestServer` helper
|
||||
|
||||
**File: `cmd/server/tests/dashboard_test.go`** (new file)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDashboardAPI_GetSections(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
adminToken := loginTestUser(t, setup.Server, setup.DB)
|
||||
|
||||
// Create test library with media items
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test Ebooks Library", "ebooks")
|
||||
|
||||
t.Run("GetSections_AsAdmin", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
sections, exists := result["sections"]
|
||||
assert.True(t, exists, "Response should contain sections")
|
||||
assert.NotNil(t, sections)
|
||||
|
||||
// Verify section structure
|
||||
sectionsArray := sections.([]interface{})
|
||||
assert.Greater(t, len(sectionsArray), 0, "Should have at least one section")
|
||||
|
||||
// Verify smart sections exist
|
||||
sectionKeys := make(map[string]bool)
|
||||
for _, s := range sectionsArray {
|
||||
section := s.(map[string]interface{})
|
||||
key := section["id"].(string)
|
||||
sectionKeys[key] = true
|
||||
|
||||
// Verify structure
|
||||
assert.Contains(t, section, "type")
|
||||
assert.Contains(t, section, "title")
|
||||
assert.Contains(t, section, "icon")
|
||||
assert.Contains(t, section, "items")
|
||||
}
|
||||
|
||||
// Check for expected smart sections
|
||||
assert.True(t, sectionKeys["continue-reading"] || sectionKeys["recently-added"],
|
||||
"Should have at least one smart section")
|
||||
})
|
||||
|
||||
t.Run("GetSections_WithoutAuth", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID, nil)
|
||||
// No authorization header
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetSections_MissingLibraryID", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetSections_InvalidLibraryID", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id=invalid-uuid", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("GetSections_WithLimit", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID+"&limit=10", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
sections := result["sections"].([]interface{})
|
||||
for _, s := range sections {
|
||||
section := s.(map[string]interface{})
|
||||
items := section["items"].([]interface{})
|
||||
assert.LessOrEqual(t, len(items), 10, "Should respect limit parameter")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetSections_WithRegularUser", func(t *testing.T) {
|
||||
// Get regular user token
|
||||
regularToken := loginRegularUser(t, setup.Server, setup.DB)
|
||||
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+regularToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashboardAPI_UserPreferences(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
adminToken := loginTestUser(t, setup.Server, setup.DB)
|
||||
|
||||
// Create test library
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test Library", "ebooks")
|
||||
|
||||
t.Run("GetSections_WithHiddenSections", func(t *testing.T) {
|
||||
// First, save preferences to hide a section
|
||||
userUUID := getUserUUIDFromToken(t, setup.DB, adminToken)
|
||||
libUUID := parseUUID(t, libraryID)
|
||||
|
||||
// Save dashboard preferences with hidden sections
|
||||
updateDashboardPreferences(t, setup.DB, userUUID, libUUID, map[string]interface{}{
|
||||
"hidden_sections": []string{"recently-added"},
|
||||
})
|
||||
|
||||
// Now get sections - "recently-added" should be hidden
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
sections := result["sections"].([]interface{})
|
||||
|
||||
// Verify "recently-added" is not in response
|
||||
for _, s := range sections {
|
||||
section := s.(map[string]interface{})
|
||||
sectionID := section["id"].(string)
|
||||
assert.NotEqual(t, "recently-added", sectionID, "Recently added should be hidden")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetSections_WithCustomOrder", func(t *testing.T) {
|
||||
userUUID := getUserUUIDFromToken(t, setup.DB, adminToken)
|
||||
libUUID := parseUUID(t, libraryID)
|
||||
|
||||
// Save dashboard preferences with custom order
|
||||
customOrder := []string{"recently-read", "continue-reading", "in-progress"}
|
||||
updateDashboardPreferences(t, setup.DB, userUUID, libUUID, map[string]interface{}{
|
||||
"section_order": customOrder,
|
||||
})
|
||||
|
||||
// Get sections - should return in custom order
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/api/dashboard/sections?library_id="+libraryID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
sections := result["sections"].([]interface{})
|
||||
|
||||
// Verify order matches custom order (for sections that exist)
|
||||
sectionOrder := make([]string, 0)
|
||||
for _, s := range sections {
|
||||
section := s.(map[string]interface{})
|
||||
sectionID := section["id"].(string)
|
||||
sectionOrder = append(sectionOrder, sectionID)
|
||||
}
|
||||
|
||||
// First section should be "recently-read" if it exists
|
||||
if len(sectionOrder) > 0 {
|
||||
assert.Equal(t, "recently-read", sectionOrder[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDashboardSSR_Page(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
adminToken := loginTestUser(t, setup.Server, setup.DB)
|
||||
|
||||
// Create test library
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
libraryID := deviceSetup.CreateLibrary(t, "Test Library", "ebooks")
|
||||
|
||||
t.Run("GetDashboardPage_AsAdmin", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/dashboard?library_id="+libraryID, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+adminToken)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
assert.Contains(t, resp.Header.Get("Content-Type"), "text/html")
|
||||
|
||||
// Verify HTML contains dashboard elements
|
||||
body := new(bytes.Buffer)
|
||||
body.ReadFrom(resp.Body)
|
||||
html := body.String()
|
||||
|
||||
assert.Contains(t, html, "dashboard-section")
|
||||
assert.Contains(t, html, "carousel-track")
|
||||
})
|
||||
|
||||
t.Run("GetDashboardPage_WithoutAuth", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", setup.Server.URL+"/dashboard?library_id="+libraryID, nil)
|
||||
// No authorization header
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// Helper functions for dashboard tests
|
||||
|
||||
func updateDashboardPreferences(t *testing.T, db *database.Queries, userID, libraryID uuid.UUID, prefs map[string]interface{}) {
|
||||
hiddenSections := prefs["hidden_sections"].([]string)
|
||||
sectionOrder := prefs["section_order"].([]string)
|
||||
|
||||
_, err := db.UpsertDashboardPreferences(context.Background(), database.UpsertDashboardPreferencesParams{
|
||||
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
||||
LibraryID: pgtype.UUID{Bytes: libraryID, Valid: true},
|
||||
HiddenSections: hiddenSections,
|
||||
SectionOrder: sectionOrder,
|
||||
ItemsPerSection: pgtype.Int4{Int32: 20, Valid: true},
|
||||
})
|
||||
require.NoError(t, err, "Failed to update dashboard preferences")
|
||||
}
|
||||
|
||||
func getUserUUIDFromToken(t *testing.T, db *database.Queries, token string) uuid.UUID {
|
||||
// Parse JWT and extract user ID
|
||||
// This would use the same logic as the JWT middleware
|
||||
// For now, return test user UUID from database
|
||||
user, err := db.GetUserByEmail(context.Background(), "testuser@example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
return uuid.UUID(user.ID.Bytes[0:16])
|
||||
}
|
||||
|
||||
func parseUUID(t *testing.T, uuidStr string) uuid.UUID {
|
||||
id, err := uuid.Parse(uuidStr)
|
||||
require.NoError(t, err)
|
||||
return id
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points**:
|
||||
- ✅ Integration tests in `cmd/server/tests/`
|
||||
- ✅ Uses `setupTestServer(t)` helper (from `test_helpers.go`)
|
||||
- ✅ Three-context testing (no auth, user, admin)
|
||||
- ✅ Tests both JSON API (`/api/dashboard/sections`) and SSR (`/dashboard`)
|
||||
- ✅ Tests user preferences (hidden sections, custom order)
|
||||
- ✅ Follows existing test patterns (see `auth_test.go`, `collections_bulk_test.go`)
|
||||
- ✅ Uses `require.NoError` for setup, `assert.Equal` for verification
|
||||
|
||||
---
|
||||
|
||||
## Summary: Key Changes from Original Carousel Dashboard Plan
|
||||
|
||||
### ✅ **What's Unchanged** (Phases 1-3, 7-11):
|
||||
@@ -2009,7 +2516,8 @@ on('click', '[data-action="open-dashboard-settings"]', () => {
|
||||
| **Dashboard Phase 7-9** | 6-7 hours | TypeScript Conversion | Templates + Settings |
|
||||
| **Dashboard Phase 10-11** | 2-3 hours | TypeScript Conversion + Dashboard 1-9 | TypeScript modules |
|
||||
| **Dashboard Phase 12-13** | 1-2 hours | Dashboard 1-11 | Documentation + Bruno tests |
|
||||
| **Total** | **19-25 days** | | Complete TypeScript + Carousel Dashboard |
|
||||
| **Dashboard Phase 14-15** | 4-6 hours | Dashboard 1-13 | Unit tests + Integration tests |
|
||||
| **Total** | **23-31 days** | | Complete TypeScript + Carousel Dashboard + Tests |
|
||||
|
||||
---
|
||||
|
||||
@@ -2048,6 +2556,26 @@ on('click', '[data-action="open-dashboard-settings"]', () => {
|
||||
- [ ] Three-context testing verified
|
||||
- [ ] Documentation matches implementation
|
||||
|
||||
### Unit Tests (Phase 14):
|
||||
- [ ] Service layer tests (`internal/services/dashboard_service_test.go`)
|
||||
- [ ] Handler helper tests (`internal/handlers/dashboard_test.go`)
|
||||
- [ ] filterHiddenSections() tested
|
||||
- [ ] reorderSections() tested
|
||||
- [ ] getSectionType() tested
|
||||
- [ ] getSectionTitle() tested
|
||||
- [ ] getSectionIcon() tested
|
||||
- [ ] All unit tests passing (`go test ./internal/services/... ./internal/handlers/...`)
|
||||
|
||||
### Integration Tests (Phase 15):
|
||||
- [ ] Integration tests created (`cmd/server/tests/dashboard_test.go`)
|
||||
- [ ] Uses setupTestServer() helper
|
||||
- [ ] Three-context testing (no auth, user, admin)
|
||||
- [ ] GET /api/dashboard/sections tested
|
||||
- [ ] User preferences tested (hidden sections, custom order)
|
||||
- [ ] SSR /dashboard tested
|
||||
- [ ] Error cases tested (missing library_id, invalid UUID)
|
||||
- [ ] All integration tests passing (`go test ./cmd/server/tests/...`)
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2025-02-17*
|
||||
@@ -2055,3 +2583,4 @@ on('click', '[data-action="open-dashboard-settings"]', () => {
|
||||
*Follows: PROJECT_GUIDELINES.md + TYPESCRIPT_CONVERSION_PLAN.md*
|
||||
*Architecture: Hybrid SSR + Generic API with Single Source of Truth*
|
||||
*Key Changes: Added `/api/dashboard/sections` JSON endpoint, user preferences applied in service layer*
|
||||
*Testing: Unit tests alongside files, Integration tests in cmd/server/tests/ with setupTestServer helper*
|
||||
|
||||
Reference in New Issue
Block a user