9240a863d1
Part 1: Shared Utilities - Create internal/fileutil package with FindDataFile() and LoadJSON() - Create internal/lang package with language constants and validation - Eliminate 46 lines of code duplication between cv/loader.go and ui/loader.go - Simplify cv/loader.go from 69 to 36 lines (-48%) - Simplify ui/loader.go from 56 to 24 lines (-57%) Part 2: Validation Layer - Add comprehensive validation in internal/models/cv/validation.go - Validate Personal (name, email format, URLs) - Validate Experience (required fields, dates) - Validate Education (required fields) - Validate Skills (proficiency ranges 1-5, categories) - Validate Languages (proficiency levels 1-5) - Validate Projects (title, URLs) - Validate Meta (version, language) - Integrate validation into LoadCV() - automatic on load - Create ValidationError and ValidationErrors types for clear error reporting - Report all validation errors at once (better UX) Testing: - Add comprehensive tests for fileutil package (FindDataFile, LoadJSON) - Add tests for lang package (IsValid, Validate, All) - Add 280+ validation test cases covering edge cases - All tests pass with real CV data (cv-en.json, cv-es.json) - Fixed validation to allow both URLs and local paths for gitRepoUrl Documentation: - Create _go-learning/refactorings/002-shared-utilities-validation.md - Document architecture, benefits, testing, and interview talking points - Explain WHY decisions were made (DRY, type safety, data integrity) Benefits: - DRY: Single source of truth for utilities - Type safety: Language constants instead of magic strings - Data integrity: Validation catches errors at load time - Better errors: Clear messages showing all issues at once - Maintainability: Centralized utilities easier to update
648 lines
14 KiB
Go
648 lines
14 KiB
Go
package cv_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/juanatsap/cv-site/internal/models/cv"
|
|
)
|
|
|
|
func TestPersonal_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
personal cv.Personal
|
|
wantErr bool
|
|
errField string
|
|
}{
|
|
{
|
|
name: "Valid - All fields correct",
|
|
personal: cv.Personal{
|
|
Name: "John Doe",
|
|
Email: "john@example.com",
|
|
LinkedIn: "https://linkedin.com/in/johndoe",
|
|
GitHub: "https://github.com/johndoe",
|
|
Website: "https://johndoe.com",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Invalid - Empty name",
|
|
personal: cv.Personal{
|
|
Name: "",
|
|
Email: "john@example.com",
|
|
},
|
|
wantErr: true,
|
|
errField: "name",
|
|
},
|
|
{
|
|
name: "Invalid - Empty email",
|
|
personal: cv.Personal{
|
|
Name: "John Doe",
|
|
Email: "",
|
|
},
|
|
wantErr: true,
|
|
errField: "email",
|
|
},
|
|
{
|
|
name: "Invalid - Bad email format",
|
|
personal: cv.Personal{
|
|
Name: "John Doe",
|
|
Email: "not-an-email",
|
|
},
|
|
wantErr: true,
|
|
errField: "email",
|
|
},
|
|
{
|
|
name: "Invalid - Bad LinkedIn URL",
|
|
personal: cv.Personal{
|
|
Name: "John Doe",
|
|
Email: "john@example.com",
|
|
LinkedIn: "not-a-url",
|
|
},
|
|
wantErr: true,
|
|
errField: "linkedin",
|
|
},
|
|
{
|
|
name: "Valid - Optional fields empty",
|
|
personal: cv.Personal{
|
|
Name: "John Doe",
|
|
Email: "john@example.com",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.personal.Validate()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Personal.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if tt.wantErr && err != nil {
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.errField) {
|
|
t.Errorf("Error should mention field '%s', got: %s", tt.errField, errMsg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExperience_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
experience cv.Experience
|
|
wantErr bool
|
|
errField string
|
|
}{
|
|
{
|
|
name: "Valid - Current position",
|
|
experience: cv.Experience{
|
|
Position: "Software Engineer",
|
|
Company: "Tech Corp",
|
|
StartDate: "2020-01",
|
|
Current: true,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Valid - Past position with end date",
|
|
experience: cv.Experience{
|
|
Position: "Software Engineer",
|
|
Company: "Tech Corp",
|
|
StartDate: "2020-01",
|
|
EndDate: "2023-12",
|
|
Current: false,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Invalid - Empty position",
|
|
experience: cv.Experience{
|
|
Position: "",
|
|
Company: "Tech Corp",
|
|
StartDate: "2020-01",
|
|
},
|
|
wantErr: true,
|
|
errField: "position",
|
|
},
|
|
{
|
|
name: "Invalid - Empty company",
|
|
experience: cv.Experience{
|
|
Position: "Software Engineer",
|
|
Company: "",
|
|
StartDate: "2020-01",
|
|
},
|
|
wantErr: true,
|
|
errField: "company",
|
|
},
|
|
{
|
|
name: "Invalid - Missing end date for past position",
|
|
experience: cv.Experience{
|
|
Position: "Software Engineer",
|
|
Company: "Tech Corp",
|
|
StartDate: "2020-01",
|
|
Current: false,
|
|
EndDate: "",
|
|
},
|
|
wantErr: true,
|
|
errField: "endDate",
|
|
},
|
|
{
|
|
name: "Invalid - Bad company URL",
|
|
experience: cv.Experience{
|
|
Position: "Software Engineer",
|
|
Company: "Tech Corp",
|
|
StartDate: "2020-01",
|
|
Current: true,
|
|
CompanyURL: "not-a-url",
|
|
},
|
|
wantErr: true,
|
|
errField: "companyURL",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.experience.Validate()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Experience.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if tt.wantErr && err != nil {
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.errField) {
|
|
t.Errorf("Error should mention field '%s', got: %s", tt.errField, errMsg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEducation_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
education cv.Education
|
|
wantErr bool
|
|
errField string
|
|
}{
|
|
{
|
|
name: "Valid - Complete education",
|
|
education: cv.Education{
|
|
Degree: "Bachelor of Science",
|
|
Institution: "University of Technology",
|
|
StartDate: "2015-09",
|
|
EndDate: "2019-06",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Invalid - Empty degree",
|
|
education: cv.Education{
|
|
Degree: "",
|
|
Institution: "University of Technology",
|
|
StartDate: "2015-09",
|
|
EndDate: "2019-06",
|
|
},
|
|
wantErr: true,
|
|
errField: "degree",
|
|
},
|
|
{
|
|
name: "Invalid - Empty institution",
|
|
education: cv.Education{
|
|
Degree: "Bachelor of Science",
|
|
Institution: "",
|
|
StartDate: "2015-09",
|
|
EndDate: "2019-06",
|
|
},
|
|
wantErr: true,
|
|
errField: "institution",
|
|
},
|
|
{
|
|
name: "Invalid - Missing start date",
|
|
education: cv.Education{
|
|
Degree: "Bachelor of Science",
|
|
Institution: "University of Technology",
|
|
StartDate: "",
|
|
EndDate: "2019-06",
|
|
},
|
|
wantErr: true,
|
|
errField: "startDate",
|
|
},
|
|
{
|
|
name: "Invalid - Missing end date",
|
|
education: cv.Education{
|
|
Degree: "Bachelor of Science",
|
|
Institution: "University of Technology",
|
|
StartDate: "2015-09",
|
|
EndDate: "",
|
|
},
|
|
wantErr: true,
|
|
errField: "endDate",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.education.Validate()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Education.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if tt.wantErr && err != nil {
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.errField) {
|
|
t.Errorf("Error should mention field '%s', got: %s", tt.errField, errMsg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSkills_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
skills cv.Skills
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "Valid - Complete skills",
|
|
skills: cv.Skills{
|
|
Technical: []cv.SkillCategory{
|
|
{
|
|
Category: "Backend",
|
|
Proficiency: 4,
|
|
Items: []string{"Go", "Python"},
|
|
Sidebar: "left",
|
|
},
|
|
},
|
|
SoftSkills: []string{"Communication", "Leadership"},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Invalid - Empty category name",
|
|
skills: cv.Skills{
|
|
Technical: []cv.SkillCategory{
|
|
{
|
|
Category: "",
|
|
Proficiency: 4,
|
|
Items: []string{"Go"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "category name is required",
|
|
},
|
|
{
|
|
name: "Invalid - Proficiency too low",
|
|
skills: cv.Skills{
|
|
Technical: []cv.SkillCategory{
|
|
{
|
|
Category: "Backend",
|
|
Proficiency: 0,
|
|
Items: []string{"Go"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "proficiency must be between 1 and 5",
|
|
},
|
|
{
|
|
name: "Invalid - Proficiency too high",
|
|
skills: cv.Skills{
|
|
Technical: []cv.SkillCategory{
|
|
{
|
|
Category: "Backend",
|
|
Proficiency: 6,
|
|
Items: []string{"Go"},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "proficiency must be between 1 and 5",
|
|
},
|
|
{
|
|
name: "Invalid - No skill items",
|
|
skills: cv.Skills{
|
|
Technical: []cv.SkillCategory{
|
|
{
|
|
Category: "Backend",
|
|
Proficiency: 4,
|
|
Items: []string{},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "at least one skill item is required",
|
|
},
|
|
{
|
|
name: "Invalid - Invalid sidebar value",
|
|
skills: cv.Skills{
|
|
Technical: []cv.SkillCategory{
|
|
{
|
|
Category: "Backend",
|
|
Proficiency: 4,
|
|
Items: []string{"Go"},
|
|
Sidebar: "middle",
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
errMsg: "sidebar must be",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.skills.Validate()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Skills.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if tt.wantErr && err != nil {
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.errMsg) {
|
|
t.Errorf("Error should contain '%s', got: %s", tt.errMsg, errMsg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLanguage_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
language cv.Language
|
|
wantErr bool
|
|
errField string
|
|
}{
|
|
{
|
|
name: "Valid - Complete language",
|
|
language: cv.Language{
|
|
Language: "English",
|
|
Proficiency: "Native",
|
|
Level: 5,
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Invalid - Empty language name",
|
|
language: cv.Language{
|
|
Language: "",
|
|
Proficiency: "Native",
|
|
Level: 5,
|
|
},
|
|
wantErr: true,
|
|
errField: "language",
|
|
},
|
|
{
|
|
name: "Invalid - Empty proficiency",
|
|
language: cv.Language{
|
|
Language: "English",
|
|
Proficiency: "",
|
|
Level: 5,
|
|
},
|
|
wantErr: true,
|
|
errField: "proficiency",
|
|
},
|
|
{
|
|
name: "Invalid - Level too low",
|
|
language: cv.Language{
|
|
Language: "English",
|
|
Proficiency: "Beginner",
|
|
Level: 0,
|
|
},
|
|
wantErr: true,
|
|
errField: "level",
|
|
},
|
|
{
|
|
name: "Invalid - Level too high",
|
|
language: cv.Language{
|
|
Language: "English",
|
|
Proficiency: "Native",
|
|
Level: 6,
|
|
},
|
|
wantErr: true,
|
|
errField: "level",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.language.Validate()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Language.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if tt.wantErr && err != nil {
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.errField) {
|
|
t.Errorf("Error should mention field '%s', got: %s", tt.errField, errMsg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProject_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
project cv.Project
|
|
wantErr bool
|
|
errMsg string
|
|
}{
|
|
{
|
|
name: "Valid - Complete project with URL",
|
|
project: cv.Project{
|
|
Title: "My Project",
|
|
URL: "https://project.com",
|
|
GitRepoUrl: "https://github.com/user/project",
|
|
Technologies: []string{"Go", "React"},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Valid - Project with local git path",
|
|
project: cv.Project{
|
|
Title: "Local Project",
|
|
URL: "https://project.com",
|
|
GitRepoUrl: "/Users/user/projects/myproject",
|
|
Technologies: []string{"Go"},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Invalid - Empty title",
|
|
project: cv.Project{
|
|
Title: "",
|
|
URL: "https://project.com",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "title is required",
|
|
},
|
|
{
|
|
name: "Invalid - Bad project URL",
|
|
project: cv.Project{
|
|
Title: "My Project",
|
|
URL: "not-a-url",
|
|
},
|
|
wantErr: true,
|
|
errMsg: "url",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.project.Validate()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Project.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if tt.wantErr && err != nil {
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.errMsg) {
|
|
t.Errorf("Error should contain '%s', got: %s", tt.errMsg, errMsg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMeta_Validate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
meta cv.Meta
|
|
wantErr bool
|
|
errField string
|
|
}{
|
|
{
|
|
name: "Valid - Complete meta",
|
|
meta: cv.Meta{
|
|
Version: "1.0",
|
|
Language: "en",
|
|
LastUpdated: "2024-01-01",
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "Invalid - Empty version",
|
|
meta: cv.Meta{
|
|
Version: "",
|
|
Language: "en",
|
|
},
|
|
wantErr: true,
|
|
errField: "version",
|
|
},
|
|
{
|
|
name: "Invalid - Empty language",
|
|
meta: cv.Meta{
|
|
Version: "1.0",
|
|
Language: "",
|
|
},
|
|
wantErr: true,
|
|
errField: "language",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := tt.meta.Validate()
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("Meta.Validate() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if tt.wantErr && err != nil {
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, tt.errField) {
|
|
t.Errorf("Error should mention field '%s', got: %s", tt.errField, errMsg)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCV_Validate(t *testing.T) {
|
|
t.Run("Valid - Complete CV", func(t *testing.T) {
|
|
validCV := &cv.CV{
|
|
Personal: cv.Personal{
|
|
Name: "John Doe",
|
|
Email: "john@example.com",
|
|
},
|
|
Skills: cv.Skills{
|
|
Technical: []cv.SkillCategory{
|
|
{
|
|
Category: "Backend",
|
|
Proficiency: 4,
|
|
Items: []string{"Go"},
|
|
},
|
|
},
|
|
},
|
|
Meta: cv.Meta{
|
|
Version: "1.0",
|
|
Language: "en",
|
|
},
|
|
}
|
|
|
|
err := validCV.Validate()
|
|
if err != nil {
|
|
t.Errorf("CV.Validate() should not error on valid CV, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid - Multiple validation errors", func(t *testing.T) {
|
|
invalidCV := &cv.CV{
|
|
Personal: cv.Personal{
|
|
Name: "", // Invalid
|
|
Email: "not-an-email", // Invalid
|
|
},
|
|
Experience: []cv.Experience{
|
|
{
|
|
Position: "", // Invalid
|
|
Company: "Tech Corp",
|
|
StartDate: "2020-01",
|
|
},
|
|
},
|
|
Skills: cv.Skills{
|
|
Technical: []cv.SkillCategory{
|
|
{
|
|
Category: "Backend",
|
|
Proficiency: 10, // Invalid
|
|
Items: []string{"Go"},
|
|
},
|
|
},
|
|
},
|
|
Meta: cv.Meta{
|
|
Version: "", // Invalid
|
|
Language: "en",
|
|
},
|
|
}
|
|
|
|
err := invalidCV.Validate()
|
|
if err == nil {
|
|
t.Error("CV.Validate() should error on invalid CV")
|
|
return
|
|
}
|
|
|
|
// Check that multiple errors are reported
|
|
errMsg := err.Error()
|
|
if !strings.Contains(errMsg, "name") {
|
|
t.Error("Error should mention 'name' field")
|
|
}
|
|
if !strings.Contains(errMsg, "email") {
|
|
t.Error("Error should mention 'email' field")
|
|
}
|
|
if !strings.Contains(errMsg, "position") {
|
|
t.Error("Error should mention 'position' field")
|
|
}
|
|
if !strings.Contains(errMsg, "proficiency") {
|
|
t.Error("Error should mention 'proficiency' field")
|
|
}
|
|
if !strings.Contains(errMsg, "version") {
|
|
t.Error("Error should mention 'version' field")
|
|
}
|
|
})
|
|
}
|