initial commit

This commit is contained in:
m.zare
2026-04-10 18:25:21 +03:30
commit 77ca6c34a3
263 changed files with 34470 additions and 0 deletions

View File

@@ -0,0 +1,196 @@
package profile
import (
"encoding/json"
"github.com/google/uuid"
domainProfile "base/internal/domain/profile"
)
func toProfileModel(profile *domainProfile.Profile) (*Model, error) {
pageSectionOrder, err := json.Marshal(profile.PageSectionOrder)
if err != nil {
return nil, err
}
var roleID *uuid.UUID
var roleName *string
roleLevel := ""
if profile.Hero.Role != nil {
roleLevel = profile.Hero.Role.Level
if profile.Hero.Role.ID != uuid.Nil {
roleID = &profile.Hero.Role.ID
roleName = &profile.Hero.Role.Title
}
}
return &Model{
ID: profile.ID,
UserID: profile.UserID,
Handle: profile.Handle,
RoleID: roleID,
RoleName: roleName,
RoleLevel: roleLevel,
FirstName: profile.Hero.FirstName,
LastName: profile.Hero.LastName,
Company: profile.Hero.Company,
ShortDescription: profile.Hero.ShortDescription,
ResumeLink: profile.Hero.ResumeLink,
CTAEnabled: profile.Hero.CTAEnabled,
Avatar: profile.Hero.Avatar,
ProfilePicture: profile.About.ProfilePicture,
About: profile.About.About,
Email: profile.Contact.Email,
Phone: profile.Contact.Phone,
VisibilityLevel: profile.PageSetting.VisibilityLevel,
PageSectionOrder: pageSectionOrder,
CreatedAt: profile.CreatedAt,
UpdatedAt: profile.UpdatedAt,
}, nil
}
func toProfileDomain(model *Model, skills []domainProfile.Skill, socialLinks []domainProfile.SocialLink, achievements []domainProfile.Achievement) (*domainProfile.Profile, error) {
var pageSectionOrder map[string]int
if len(model.PageSectionOrder) > 0 {
if err := json.Unmarshal(model.PageSectionOrder, &pageSectionOrder); err != nil {
return nil, err
}
}
var role *domainProfile.Role
if model.RoleID != nil && *model.RoleID != uuid.Nil {
title := ""
if model.Role != nil {
title = model.Role.Title
} else if model.RoleName != nil {
title = *model.RoleName
}
role = &domainProfile.Role{
ID: *model.RoleID,
Title: title,
Level: model.RoleLevel,
}
} else if model.RoleLevel != "" {
role = &domainProfile.Role{Level: model.RoleLevel}
}
hero := domainProfile.Hero{
Role: role,
FirstName: model.FirstName,
LastName: model.LastName,
Company: model.Company,
ShortDescription: model.ShortDescription,
ResumeLink: model.ResumeLink,
CTAEnabled: model.CTAEnabled,
Avatar: model.Avatar,
}
about := domainProfile.About{
ProfilePicture: model.ProfilePicture,
About: model.About,
Achievements: achievements,
}
contact := domainProfile.Contact{
Email: model.Email,
Phone: model.Phone,
SocialLinks: socialLinks,
}
pageSetting := domainProfile.PageSetting{
VisibilityLevel: model.VisibilityLevel,
}
return &domainProfile.Profile{
ID: model.ID,
UserID: model.UserID,
Handle: model.Handle,
PageSectionOrder: pageSectionOrder,
Hero: hero,
About: about,
Skills: skills,
Contact: contact,
PageSetting: pageSetting,
CreatedAt: model.CreatedAt,
UpdatedAt: model.UpdatedAt,
}, nil
}
func toSkillModels(profileID uuid.UUID, skills []domainProfile.Skill) []SkillModel {
models := make([]SkillModel, len(skills))
for i, skill := range skills {
models[i] = SkillModel{
ProfileID: profileID,
SkillName: skill.SkillName,
Level: skill.Level,
}
}
return models
}
func toSkillDomains(models []SkillModel) []domainProfile.Skill {
skills := make([]domainProfile.Skill, len(models))
for i, model := range models {
skills[i] = domainProfile.Skill{
SkillName: model.SkillName,
Level: model.Level,
}
}
return skills
}
func toSocialLinkModels(profileID uuid.UUID, socialLinks []domainProfile.SocialLink) []SocialLinkModel {
models := make([]SocialLinkModel, len(socialLinks))
for i, link := range socialLinks {
models[i] = SocialLinkModel{
ProfileID: profileID,
LinkType: link.LinkType,
Link: link.Link,
}
}
return models
}
func toSocialLinkDomains(models []SocialLinkModel) []domainProfile.SocialLink {
links := make([]domainProfile.SocialLink, len(models))
for i, model := range models {
links[i] = domainProfile.SocialLink{
LinkType: model.LinkType,
Link: model.Link,
}
}
return links
}
func toAchievementModels(profileID uuid.UUID, achievements []domainProfile.Achievement) []AchievementModel {
models := make([]AchievementModel, len(achievements))
for i, achievement := range achievements {
models[i] = AchievementModel{
ProfileID: profileID,
Title: achievement.Title,
Value: achievement.Value,
Enabled: achievement.Enabled,
}
}
return models
}
func toAchievementDomains(models []AchievementModel) []domainProfile.Achievement {
achievements := make([]domainProfile.Achievement, len(models))
for i, model := range models {
achievements[i] = domainProfile.Achievement{
Title: model.Title,
Value: model.Value,
Enabled: model.Enabled,
}
}
return achievements
}
func copyProfileFromModel(profile *domainProfile.Profile, model *Model) error {
profile.ID = model.ID
profile.Handle = model.Handle
return nil
}

View File

@@ -0,0 +1,315 @@
package profile
import (
"context"
"errors"
"github.com/google/uuid"
"go.uber.org/fx"
"gorm.io/gorm"
domainProfile "base/internal/domain/profile"
)
type profileRepository struct {
db *gorm.DB
}
func NewProfileRepository(lc fx.Lifecycle, db *gorm.DB) domainProfile.Repository {
lc.Append(
fx.Hook{
OnStart: func(ctx context.Context) error {
return nil
},
OnStop: func(ctx context.Context) error {
return nil
},
})
return &profileRepository{db: db}
}
func (r *profileRepository) Create(ctx context.Context, profile *domainProfile.Profile) error {
model, err := toProfileModel(profile)
if err != nil {
return err
}
// Start a transaction
tx := r.db.WithContext(ctx).Begin()
if tx.Error != nil {
return tx.Error
}
defer tx.Rollback()
// Create profile
if err := tx.Create(model).Error; err != nil {
return err
}
// Create skills if any
if len(profile.Skills) > 0 {
skillModels := toSkillModels(model.ID, profile.Skills)
if err := tx.Create(&skillModels).Error; err != nil {
return err
}
}
// Create social links if any
if len(profile.Contact.SocialLinks) > 0 {
socialLinkModels := toSocialLinkModels(model.ID, profile.Contact.SocialLinks)
if err := tx.Create(&socialLinkModels).Error; err != nil {
return err
}
}
// Create achievements if any
if len(profile.About.Achievements) > 0 {
achievementModels := toAchievementModels(model.ID, profile.About.Achievements)
if err := tx.Create(&achievementModels).Error; err != nil {
return err
}
}
if err := tx.Commit().Error; err != nil {
return err
}
return copyProfileFromModel(profile, model)
}
func (r *profileRepository) loadRelatedData(ctx context.Context, profileID uuid.UUID) ([]domainProfile.Skill, []domainProfile.SocialLink, []domainProfile.Achievement, error) {
// Load skills
var skillModels []SkillModel
if err := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Find(&skillModels).Error; err != nil {
return nil, nil, nil, err
}
skills := toSkillDomains(skillModels)
// Load social links
var socialLinkModels []SocialLinkModel
if err := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Find(&socialLinkModels).Error; err != nil {
return nil, nil, nil, err
}
socialLinks := toSocialLinkDomains(socialLinkModels)
// Load achievements
var achievementModels []AchievementModel
if err := r.db.WithContext(ctx).Where("profile_id = ?", profileID).Find(&achievementModels).Error; err != nil {
return nil, nil, nil, err
}
achievements := toAchievementDomains(achievementModels)
return skills, socialLinks, achievements, nil
}
func (r *profileRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainProfile.Profile, error) {
var model Model
if err := r.db.WithContext(ctx).Preload("Role").Where("id = ?", id).First(&model).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("profile not found")
}
return nil, err
}
skills, socialLinks, achievements, err := r.loadRelatedData(ctx, model.ID)
if err != nil {
return nil, err
}
return toProfileDomain(&model, skills, socialLinks, achievements)
}
func (r *profileRepository) FindByHandle(ctx context.Context, handle string) (*domainProfile.Profile, error) {
var model Model
if err := r.db.WithContext(ctx).Preload("Role").Where("handle = ?", handle).First(&model).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("profile not found")
}
return nil, err
}
skills, socialLinks, achievements, err := r.loadRelatedData(ctx, model.ID)
if err != nil {
return nil, err
}
return toProfileDomain(&model, skills, socialLinks, achievements)
}
func (r *profileRepository) FindByUserID(ctx context.Context, userID uuid.UUID) (*domainProfile.Profile, error) {
var model Model
if err := r.db.WithContext(ctx).Preload("Role").Where("user_id = ? AND user_id IS NOT NULL", userID).First(&model).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("profile not found")
}
return nil, err
}
skills, socialLinks, achievements, err := r.loadRelatedData(ctx, model.ID)
if err != nil {
return nil, err
}
return toProfileDomain(&model, skills, socialLinks, achievements)
}
func (r *profileRepository) Update(ctx context.Context, profile *domainProfile.Profile) error {
model, err := toProfileModel(profile)
if err != nil {
return err
}
// Start a transaction
tx := r.db.WithContext(ctx).Begin()
if tx.Error != nil {
return tx.Error
}
defer tx.Rollback()
// Update profile
if err := tx.Model(&Model{}).Where("id = ?", profile.ID).Updates(model).Error; err != nil {
return err
}
// Delete existing related data
if err := tx.Where("profile_id = ?", profile.ID).Delete(&SkillModel{}).Error; err != nil {
return err
}
if err := tx.Where("profile_id = ?", profile.ID).Delete(&SocialLinkModel{}).Error; err != nil {
return err
}
if err := tx.Where("profile_id = ?", profile.ID).Delete(&AchievementModel{}).Error; err != nil {
return err
}
// Create new skills
if len(profile.Skills) > 0 {
skillModels := toSkillModels(profile.ID, profile.Skills)
if err := tx.Create(&skillModels).Error; err != nil {
return err
}
}
// Create new social links
if len(profile.Contact.SocialLinks) > 0 {
socialLinkModels := toSocialLinkModels(profile.ID, profile.Contact.SocialLinks)
if err := tx.Create(&socialLinkModels).Error; err != nil {
return err
}
}
// Create new achievements
if len(profile.About.Achievements) > 0 {
achievementModels := toAchievementModels(profile.ID, profile.About.Achievements)
if err := tx.Create(&achievementModels).Error; err != nil {
return err
}
}
return tx.Commit().Error
}
func (r *profileRepository) Delete(ctx context.Context, profile *domainProfile.Profile) error {
// Start a transaction
tx := r.db.WithContext(ctx).Begin()
if tx.Error != nil {
return tx.Error
}
defer tx.Rollback()
// Delete related data first
if err := tx.Where("profile_id = ?", profile.ID).Delete(&SkillModel{}).Error; err != nil {
return err
}
if err := tx.Where("profile_id = ?", profile.ID).Delete(&SocialLinkModel{}).Error; err != nil {
return err
}
if err := tx.Where("profile_id = ?", profile.ID).Delete(&AchievementModel{}).Error; err != nil {
return err
}
// Delete profile
if err := tx.Delete(&Model{}, "id = ?", profile.ID).Error; err != nil {
return err
}
return tx.Commit().Error
}
// buildBaseQuery applies common filters to a query
func (r *profileRepository) buildBaseQuery(ctx context.Context, filter domainProfile.Filter) *gorm.DB {
query := r.db.WithContext(ctx).Model(&Model{})
if filter.RoleID != uuid.Nil {
query = query.Where("role_id = ?", filter.RoleID)
}
if filter.FirstName != "" {
query = query.Where("LOWER(first_name) LIKE ?", "%"+filter.FirstName+"%")
}
if filter.LastName != "" {
query = query.Where("LOWER(last_name) LIKE ?", "%"+filter.LastName+"%")
}
if filter.Company != "" {
query = query.Where("LOWER(company) LIKE ?", "%"+filter.Company+"%")
}
if filter.SkillName != "" {
subQuery := r.db.WithContext(ctx).Model(&SkillModel{}).
Select("DISTINCT profile_id").
Where("LOWER(skill_name) LIKE ? AND deleted_at IS NULL", "%"+filter.SkillName+"%")
query = query.Where("profiles.id IN (?)", subQuery)
}
return query
}
func (r *profileRepository) FindAll(ctx context.Context, filter domainProfile.Filter) ([]*domainProfile.Profile, int, error) {
baseQuery := r.buildBaseQuery(ctx, filter)
var total int64
if err := baseQuery.Count(&total).Error; err != nil {
return nil, 0, err
}
query := baseQuery
offset := int((filter.Page - 1) * filter.PageSize)
limit := int(filter.PageSize)
if limit > 0 {
query = query.Limit(limit).Offset(offset)
}
if filter.SortedBy != "" {
order := "ASC"
if !filter.Ascending {
order = "DESC"
}
query = query.Order("profiles." + filter.SortedBy + " " + order)
} else {
query = query.Order("profiles.created_at DESC")
}
var models []Model
if err := query.Preload("Role").Find(&models).Error; err != nil {
return nil, 0, err
}
if len(models) == 0 {
return nil, int(total), nil
}
profiles := make([]*domainProfile.Profile, len(models))
for i, model := range models {
skills, socialLinks, achievements, err := r.loadRelatedData(ctx, model.ID)
if err != nil {
return nil, 0, err
}
profile, err := toProfileDomain(&model, skills, socialLinks, achievements)
if err != nil {
return nil, 0, err
}
profiles[i] = profile
}
return profiles, int(total), nil
}

View File

@@ -0,0 +1,870 @@
package profile
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
domainProfile "base/internal/domain/profile"
)
func TestProfileRepository_Create(t *testing.T) {
db := setupTestDB(t)
repo := createTestProfileRepository(db)
ctx := context.Background()
t.Run("create profile successfully", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "test-handle",
PageSectionOrder: map[string]int{
"hero": 1,
"about": 2,
"skills": 3,
},
Hero: domainProfile.Hero{
FirstName: "John",
LastName: "Doe",
Company: "Test Company",
ShortDescription: "Test description",
CTAEnabled: true,
},
About: domainProfile.About{
ProfilePicture: "https://example.com/pic.jpg",
About: "About me",
},
Contact: domainProfile.Contact{
Email: "john.doe@example.com",
Phone: "1234567890",
},
PageSetting: domainProfile.PageSetting{
VisibilityLevel: "public",
},
}
err := repo.Create(ctx, profile)
assert.NoError(t, err)
assert.NotEqual(t, uuid.Nil, profile.ID)
// Verify profile was created
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Equal(t, profile.Handle, found.Handle)
assert.Equal(t, profile.Hero.FirstName, found.Hero.FirstName)
assert.Equal(t, profile.Hero.LastName, found.Hero.LastName)
assert.Equal(t, profile.Contact.Email, found.Contact.Email)
})
t.Run("create profile with skills", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "test-handle-with-skills",
Hero: domainProfile.Hero{
FirstName: "Jane",
LastName: "Smith",
},
Skills: []domainProfile.Skill{
{SkillName: "Go", Level: "expert"},
{SkillName: "Python", Level: "intermediate"},
},
}
err := repo.Create(ctx, profile)
assert.NoError(t, err)
// Verify profile with skills
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Len(t, found.Skills, 2)
assert.Equal(t, "Go", found.Skills[0].SkillName)
assert.Equal(t, "expert", found.Skills[0].Level)
})
t.Run("create profile with social links", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "test-handle-with-links",
Hero: domainProfile.Hero{
FirstName: "Bob",
LastName: "Johnson",
},
Contact: domainProfile.Contact{
SocialLinks: []domainProfile.SocialLink{
{LinkType: "linkedin", Link: "https://linkedin.com/in/bob"},
{LinkType: "github", Link: "https://github.com/bob"},
},
},
}
err := repo.Create(ctx, profile)
assert.NoError(t, err)
// Verify profile with social links
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Len(t, found.Contact.SocialLinks, 2)
assert.Equal(t, "linkedin", found.Contact.SocialLinks[0].LinkType)
})
t.Run("create profile with achievements", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "test-handle-with-achievements",
Hero: domainProfile.Hero{
FirstName: "Alice",
LastName: "Williams",
},
About: domainProfile.About{
Achievements: []domainProfile.Achievement{
{Title: "Projects", Value: "50", Enabled: true},
{Title: "Clients", Value: "100", Enabled: true},
},
},
}
err := repo.Create(ctx, profile)
assert.NoError(t, err)
// Verify profile with achievements
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Len(t, found.About.Achievements, 2)
assert.Equal(t, "Projects", found.About.Achievements[0].Title)
assert.Equal(t, "50", found.About.Achievements[0].Value)
})
t.Run("create profile with duplicate handle fails", func(t *testing.T) {
handle := "duplicate-handle"
profile1 := &domainProfile.Profile{
ID: uuid.New(),
Handle: handle,
Hero: domainProfile.Hero{
FirstName: "First",
LastName: "User",
},
}
err := repo.Create(ctx, profile1)
assert.NoError(t, err)
profile2 := &domainProfile.Profile{
ID: uuid.New(),
Handle: handle,
Hero: domainProfile.Hero{
FirstName: "Second",
LastName: "User",
},
}
err = repo.Create(ctx, profile2)
assert.Error(t, err)
})
t.Run("create profile with role", func(t *testing.T) {
roleID := uuid.New()
roleName := "Software Engineer"
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "test-handle-with-role",
Hero: domainProfile.Hero{
Role: &domainProfile.Role{
ID: roleID,
Title: roleName,
},
FirstName: "Role",
LastName: "User",
},
}
err := repo.Create(ctx, profile)
assert.NoError(t, err)
// Verify profile with role
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.NotNil(t, found.Hero.Role)
assert.Equal(t, roleID, found.Hero.Role.ID)
assert.Equal(t, roleName, found.Hero.Role.Title)
})
}
func TestProfileRepository_FindByHandle(t *testing.T) {
db := setupTestDB(t)
repo := createTestProfileRepository(db)
ctx := context.Background()
t.Run("find profile by handle successfully", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "find-by-handle",
Hero: domainProfile.Hero{
FirstName: "Find",
LastName: "Handle",
},
Contact: domainProfile.Contact{
Email: "find@example.com",
},
}
err := repo.Create(ctx, profile)
require.NoError(t, err)
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.NotNil(t, found)
assert.Equal(t, profile.Handle, found.Handle)
assert.Equal(t, profile.Hero.FirstName, found.Hero.FirstName)
assert.Equal(t, profile.Contact.Email, found.Contact.Email)
})
t.Run("find non-existent profile returns error", func(t *testing.T) {
found, err := repo.FindByHandle(ctx, "non-existent-handle")
assert.Error(t, err)
assert.Nil(t, found)
assert.Contains(t, err.Error(), "profile not found")
})
t.Run("find profile with all related data", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "find-with-all-data",
Hero: domainProfile.Hero{
FirstName: "All",
LastName: "Data",
},
Skills: []domainProfile.Skill{
{SkillName: "JavaScript", Level: "advanced"},
},
Contact: domainProfile.Contact{
SocialLinks: []domainProfile.SocialLink{
{LinkType: "twitter", Link: "https://twitter.com/user"},
},
},
About: domainProfile.About{
Achievements: []domainProfile.Achievement{
{Title: "Years", Value: "10", Enabled: true},
},
},
}
err := repo.Create(ctx, profile)
require.NoError(t, err)
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Len(t, found.Skills, 1)
assert.Len(t, found.Contact.SocialLinks, 1)
assert.Len(t, found.About.Achievements, 1)
})
}
func TestProfileRepository_FindByUserID(t *testing.T) {
db := setupTestDB(t)
repo := createTestProfileRepository(db)
ctx := context.Background()
t.Run("find profile by user ID successfully", func(t *testing.T) {
userID := uuid.New()
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "find-by-user-id",
Hero: domainProfile.Hero{
FirstName: "User",
LastName: "ID",
},
}
// Create profile with user_id manually since it's not in the domain model
model, err := toProfileModel(profile)
require.NoError(t, err)
model.UserID = &userID
err = db.WithContext(ctx).Create(model).Error
require.NoError(t, err)
found, err := repo.FindByUserID(ctx, userID)
assert.NoError(t, err)
assert.NotNil(t, found)
assert.Equal(t, profile.Handle, found.Handle)
})
t.Run("find non-existent user ID returns error", func(t *testing.T) {
nonExistentUserID := uuid.New()
found, err := repo.FindByUserID(ctx, nonExistentUserID)
assert.Error(t, err)
assert.Nil(t, found)
assert.Contains(t, err.Error(), "profile not found")
})
}
func TestProfileRepository_Update(t *testing.T) {
db := setupTestDB(t)
repo := createTestProfileRepository(db)
ctx := context.Background()
t.Run("update profile successfully", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "update-profile",
Hero: domainProfile.Hero{
FirstName: "Original",
LastName: "Name",
Company: "Old Company",
},
Contact: domainProfile.Contact{
Email: "original@example.com",
},
}
err := repo.Create(ctx, profile)
require.NoError(t, err)
// Update profile
profile.Hero.FirstName = "Updated"
profile.Hero.Company = "New Company"
profile.Contact.Email = "updated@example.com"
err = repo.Update(ctx, profile)
assert.NoError(t, err)
// Verify update
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Equal(t, "Updated", found.Hero.FirstName)
assert.Equal(t, "New Company", found.Hero.Company)
assert.Equal(t, "updated@example.com", found.Contact.Email)
})
t.Run("update profile with new skills", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "update-skills",
Hero: domainProfile.Hero{
FirstName: "Skills",
LastName: "User",
},
Skills: []domainProfile.Skill{
{SkillName: "Go", Level: "beginner"},
},
}
err := repo.Create(ctx, profile)
require.NoError(t, err)
// Update with new skills
profile.Skills = []domainProfile.Skill{
{SkillName: "Go", Level: "expert"},
{SkillName: "Rust", Level: "intermediate"},
}
err = repo.Update(ctx, profile)
assert.NoError(t, err)
// Verify skills were updated
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Len(t, found.Skills, 2)
// Check that old skill is gone and new ones exist
skillMap := make(map[string]string)
for _, skill := range found.Skills {
skillMap[skill.SkillName] = skill.Level
}
assert.Equal(t, "expert", skillMap["Go"])
assert.Equal(t, "intermediate", skillMap["Rust"])
})
t.Run("update profile with new social links", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "update-links",
Hero: domainProfile.Hero{
FirstName: "Links",
LastName: "User",
},
Contact: domainProfile.Contact{
SocialLinks: []domainProfile.SocialLink{
{LinkType: "linkedin", Link: "https://linkedin.com/old"},
},
},
}
err := repo.Create(ctx, profile)
require.NoError(t, err)
// Update with new social links
profile.Contact.SocialLinks = []domainProfile.SocialLink{
{LinkType: "github", Link: "https://github.com/new"},
{LinkType: "twitter", Link: "https://twitter.com/new"},
}
err = repo.Update(ctx, profile)
assert.NoError(t, err)
// Verify social links were updated
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Len(t, found.Contact.SocialLinks, 2)
linkTypes := make(map[string]bool)
for _, link := range found.Contact.SocialLinks {
linkTypes[link.LinkType] = true
}
assert.True(t, linkTypes["github"])
assert.True(t, linkTypes["twitter"])
assert.False(t, linkTypes["linkedin"])
})
t.Run("update profile with new achievements", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "update-achievements",
Hero: domainProfile.Hero{
FirstName: "Achievements",
LastName: "User",
},
About: domainProfile.About{
Achievements: []domainProfile.Achievement{
{Title: "Old", Value: "1", Enabled: true},
},
},
}
err := repo.Create(ctx, profile)
require.NoError(t, err)
// Update with new achievements
profile.About.Achievements = []domainProfile.Achievement{
{Title: "New1", Value: "10", Enabled: true},
{Title: "New2", Value: "20", Enabled: false},
}
err = repo.Update(ctx, profile)
assert.NoError(t, err)
// Verify achievements were updated
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Len(t, found.About.Achievements, 2)
achievementMap := make(map[string]string)
for _, achievement := range found.About.Achievements {
achievementMap[achievement.Title] = achievement.Value
}
assert.Equal(t, "10", achievementMap["New1"])
assert.Equal(t, "20", achievementMap["New2"])
})
t.Run("update profile page section order", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "update-page-order",
Hero: domainProfile.Hero{
FirstName: "Page",
LastName: "Order",
},
PageSectionOrder: map[string]int{
"hero": 1,
},
}
err := repo.Create(ctx, profile)
require.NoError(t, err)
// Update page section order
profile.PageSectionOrder = map[string]int{
"about": 1,
"hero": 2,
"skills": 3,
}
err = repo.Update(ctx, profile)
assert.NoError(t, err)
// Verify page section order was updated
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.Equal(t, 1, found.PageSectionOrder["about"])
assert.Equal(t, 2, found.PageSectionOrder["hero"])
assert.Equal(t, 3, found.PageSectionOrder["skills"])
})
}
func TestProfileRepository_Delete(t *testing.T) {
db := setupTestDB(t)
repo := createTestProfileRepository(db)
ctx := context.Background()
t.Run("delete profile successfully", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "delete-profile",
Hero: domainProfile.Hero{
FirstName: "Delete",
LastName: "User",
},
Skills: []domainProfile.Skill{
{SkillName: "Go", Level: "expert"},
},
Contact: domainProfile.Contact{
SocialLinks: []domainProfile.SocialLink{
{LinkType: "github", Link: "https://github.com/user"},
},
},
About: domainProfile.About{
Achievements: []domainProfile.Achievement{
{Title: "Projects", Value: "10", Enabled: true},
},
},
}
err := repo.Create(ctx, profile)
require.NoError(t, err)
err = repo.Delete(ctx, profile)
assert.NoError(t, err)
// Verify deletion
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.Error(t, err)
assert.Nil(t, found)
assert.Contains(t, err.Error(), "profile not found")
})
}
func TestProfileRepository_FindAll(t *testing.T) {
db := setupTestDB(t)
repo := createTestProfileRepository(db)
ctx := context.Background()
// Create test profiles
roleID1 := uuid.New()
roleID2 := uuid.New()
profiles := []*domainProfile.Profile{
{
ID: uuid.New(),
Handle: "findall-1",
Hero: domainProfile.Hero{
Role: &domainProfile.Role{
ID: roleID1,
Title: "Engineer",
},
FirstName: "Alice",
LastName: "Anderson",
Company: "Tech Corp",
},
Skills: []domainProfile.Skill{
{SkillName: "Go", Level: "expert"},
{SkillName: "Python", Level: "intermediate"},
},
},
{
ID: uuid.New(),
Handle: "findall-2",
Hero: domainProfile.Hero{
Role: &domainProfile.Role{
ID: roleID1,
Title: "Engineer",
},
FirstName: "Bob",
LastName: "Brown",
Company: "Tech Corp",
},
Skills: []domainProfile.Skill{
{SkillName: "JavaScript", Level: "expert"},
},
},
{
ID: uuid.New(),
Handle: "findall-3",
Hero: domainProfile.Hero{
Role: &domainProfile.Role{
ID: roleID2,
Title: "Designer",
},
FirstName: "Charlie",
LastName: "Clark",
Company: "Design Inc",
},
Skills: []domainProfile.Skill{
{SkillName: "Figma", Level: "expert"},
},
},
}
for _, profile := range profiles {
err := repo.Create(ctx, profile)
require.NoError(t, err)
// Add small delay to ensure different timestamps
time.Sleep(10 * time.Millisecond)
}
t.Run("find all profiles without filters", func(t *testing.T) {
filter := domainProfile.Filter{
Page: 1,
PageSize: 10,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, 3)
assert.GreaterOrEqual(t, len(results), 3)
})
t.Run("find profiles by role ID", func(t *testing.T) {
filter := domainProfile.Filter{
RoleID: roleID1,
Page: 1,
PageSize: 10,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.Equal(t, 2, total)
assert.Len(t, results, 2)
for _, result := range results {
assert.NotNil(t, result.Hero.Role)
assert.Equal(t, roleID1, result.Hero.Role.ID)
}
})
t.Run("find profiles by first name", func(t *testing.T) {
filter := domainProfile.Filter{
FirstName: "alice",
Page: 1,
PageSize: 10,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.Equal(t, 1, total)
assert.Len(t, results, 1)
assert.Equal(t, "Alice", results[0].Hero.FirstName)
})
t.Run("find profiles by last name", func(t *testing.T) {
filter := domainProfile.Filter{
LastName: "brown",
Page: 1,
PageSize: 10,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.Equal(t, 1, total)
assert.Len(t, results, 1)
assert.Equal(t, "Brown", results[0].Hero.LastName)
})
t.Run("find profiles by company", func(t *testing.T) {
filter := domainProfile.Filter{
Company: "tech",
Page: 1,
PageSize: 10,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.Equal(t, 2, total)
assert.Len(t, results, 2)
for _, result := range results {
assert.Contains(t, result.Hero.Company, "Tech")
}
})
t.Run("find profiles by skill name", func(t *testing.T) {
filter := domainProfile.Filter{
SkillName: "go",
Page: 1,
PageSize: 10,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.Equal(t, 1, total)
assert.Len(t, results, 1)
assert.Equal(t, "findall-1", results[0].Handle)
// Verify the profile has the skill
hasGoSkill := false
for _, skill := range results[0].Skills {
if skill.SkillName == "Go" {
hasGoSkill = true
break
}
}
assert.True(t, hasGoSkill)
})
t.Run("find profiles with pagination", func(t *testing.T) {
filter := domainProfile.Filter{
Page: 1,
PageSize: 2,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, 3)
assert.Len(t, results, 2)
// Second page
filter.Page = 2
results2, total2, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.Equal(t, total, total2)
assert.GreaterOrEqual(t, len(results2), 1)
})
t.Run("find profiles with sorting", func(t *testing.T) {
filter := domainProfile.Filter{
Page: 1,
PageSize: 10,
SortedBy: "first_name",
Ascending: true,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.GreaterOrEqual(t, total, 3)
assert.GreaterOrEqual(t, len(results), 3)
// Verify sorting (first result should be Alice)
assert.Equal(t, "Alice", results[0].Hero.FirstName)
// Test descending order
filter.Ascending = false
results2, _, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
// Last result should be Alice (or one of the first names alphabetically)
assert.NotEqual(t, "Alice", results2[0].Hero.FirstName)
})
t.Run("find profiles with combined filters", func(t *testing.T) {
filter := domainProfile.Filter{
RoleID: roleID1,
Company: "tech",
Page: 1,
PageSize: 10,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.Equal(t, 2, total)
assert.Len(t, results, 2)
for _, result := range results {
assert.NotNil(t, result.Hero.Role)
assert.Equal(t, roleID1, result.Hero.Role.ID)
assert.Contains(t, result.Hero.Company, "Tech")
}
})
t.Run("find profiles with empty result", func(t *testing.T) {
filter := domainProfile.Filter{
FirstName: "nonexistent",
Page: 1,
PageSize: 10,
}
results, total, err := repo.FindAll(ctx, filter)
assert.NoError(t, err)
assert.Equal(t, 0, total)
assert.Len(t, results, 0)
})
}
func TestProfileRepository_PageSectionOrder(t *testing.T) {
db := setupTestDB(t)
repo := createTestProfileRepository(db)
ctx := context.Background()
t.Run("create and retrieve profile with page section order", func(t *testing.T) {
pageSectionOrder := map[string]int{
"hero": 1,
"about": 2,
"skills": 3,
"contact": 4,
}
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "page-order-test",
PageSectionOrder: pageSectionOrder,
Hero: domainProfile.Hero{
FirstName: "Page",
LastName: "Order",
},
}
err := repo.Create(ctx, profile)
assert.NoError(t, err)
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
assert.NotNil(t, found.PageSectionOrder)
assert.Equal(t, 1, found.PageSectionOrder["hero"])
assert.Equal(t, 2, found.PageSectionOrder["about"])
assert.Equal(t, 3, found.PageSectionOrder["skills"])
assert.Equal(t, 4, found.PageSectionOrder["contact"])
})
t.Run("create profile with empty page section order", func(t *testing.T) {
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "empty-page-order",
Hero: domainProfile.Hero{
FirstName: "Empty",
LastName: "Order",
},
}
err := repo.Create(ctx, profile)
assert.NoError(t, err)
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
// Empty map should be returned as empty or nil
assert.NotNil(t, found)
})
}
// Helper function to verify JSON marshaling/unmarshaling works correctly
func TestProfileRepository_JSONSerialization(t *testing.T) {
db := setupTestDB(t)
repo := createTestProfileRepository(db)
ctx := context.Background()
t.Run("verify page section order JSON serialization", func(t *testing.T) {
complexOrder := map[string]int{
"section1": 10,
"section2": 20,
"section3": 30,
}
profile := &domainProfile.Profile{
ID: uuid.New(),
Handle: "json-test",
PageSectionOrder: complexOrder,
Hero: domainProfile.Hero{
FirstName: "JSON",
LastName: "Test",
},
}
err := repo.Create(ctx, profile)
assert.NoError(t, err)
// Verify the data can be serialized/deserialized correctly
found, err := repo.FindByHandle(ctx, profile.Handle)
assert.NoError(t, err)
// Re-serialize to verify round-trip
jsonData, err := json.Marshal(found.PageSectionOrder)
assert.NoError(t, err)
var unmarshaled map[string]int
err = json.Unmarshal(jsonData, &unmarshaled)
assert.NoError(t, err)
assert.Equal(t, complexOrder, unmarshaled)
})
}

View File

@@ -0,0 +1,112 @@
package profile
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"go.uber.org/fx"
"gorm.io/gorm"
domainProfile "base/internal/domain/profile"
)
type roleRepository struct {
db *gorm.DB
}
// NewRoleRepository creates a RoleRepository for profile_roles.
func NewRoleRepository(lc fx.Lifecycle, db *gorm.DB) domainProfile.RoleRepository {
lc.Append(
fx.Hook{
OnStart: func(ctx context.Context) error { return nil },
OnStop: func(ctx context.Context) error { return nil },
})
return &roleRepository{db: db}
}
func (r *roleRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainProfile.Role, error) {
var model RoleModel
if err := r.db.WithContext(ctx).Where("id = ?", id).First(&model).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, domainProfile.ErrRoleNotFound
}
return nil, err
}
return roleModelToDomain(&model), nil
}
func (r *roleRepository) FindAll(ctx context.Context) ([]*domainProfile.Role, error) {
var models []RoleModel
if err := r.db.WithContext(ctx).Order("status DESC, title ASC").Find(&models).Error; err != nil {
return nil, err
}
out := make([]*domainProfile.Role, len(models))
for i := range models {
out[i] = roleModelToDomain(&models[i])
}
return out, nil
}
func (r *roleRepository) List(ctx context.Context, limit, offset int) ([]*domainProfile.Role, error) {
var models []RoleModel
q := r.db.WithContext(ctx).Order("status DESC, title ASC")
if limit > 0 {
q = q.Limit(limit)
}
if offset > 0 {
q = q.Offset(offset)
}
if err := q.Find(&models).Error; err != nil {
return nil, err
}
out := make([]*domainProfile.Role, len(models))
for i := range models {
out[i] = roleModelToDomain(&models[i])
}
return out, nil
}
func roleModelToDomain(m *RoleModel) *domainProfile.Role {
return &domainProfile.Role{
ID: m.ID,
Title: m.Title,
}
}
func (r *roleRepository) Create(ctx context.Context, role *domainProfile.Role) error {
now := time.Now()
model := &RoleModel{
ID: role.ID,
Title: role.Title,
CreatedAt: now,
UpdatedAt: now,
}
if err := r.db.WithContext(ctx).Create(model).Error; err != nil {
return err
}
return nil
}
func (r *roleRepository) Update(ctx context.Context, role *domainProfile.Role) error {
result := r.db.WithContext(ctx).Model(&RoleModel{}).Where("id = ?", role.ID).Updates(map[string]interface{}{"title": role.Title})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return domainProfile.ErrRoleNotFound
}
return nil
}
func (r *roleRepository) Delete(ctx context.Context, id uuid.UUID) error {
result := r.db.WithContext(ctx).Delete(&RoleModel{}, "id = ?", id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return domainProfile.ErrRoleNotFound
}
return nil
}

View File

@@ -0,0 +1,134 @@
package profile
import (
"context"
"sync"
"github.com/google/uuid"
domainProfile "base/internal/domain/profile"
)
// mockRoleData holds the mocked profile roles (matches seed data).
var mockRoleData = []*domainProfile.Role{
{ID: uuid.MustParse("0199b964-5dc0-7657-9178-2a844e23e5b5"), Title: "Data Scientist"},
{ID: uuid.MustParse("0199b964-5dc0-7a1a-94c7-d68daf420e50"), Title: "Machine Learning Engineer"},
{ID: uuid.MustParse("0199b964-5dc0-7759-8221-71f57f5b2b57"), Title: "AI Engineer"},
{ID: uuid.MustParse("0199b964-5dc0-7b79-a268-331f39c35366"), Title: "Data Engineer"},
{ID: uuid.MustParse("0199b964-5dc0-7062-b219-11733a1ab94b"), Title: "Data Analyst"},
{ID: uuid.MustParse("0199b964-5dc0-7434-b105-f2ff49573fe2"), Title: "Business Intelligence Developer"},
{ID: uuid.MustParse("0199b964-5dc0-77f8-be02-f76937f60ba6"), Title: "MLOps Engineer"},
{ID: uuid.MustParse("0199b964-5dc0-7107-907c-6c013cbc08b9"), Title: "AI Product Manager"},
{ID: uuid.MustParse("0199b964-5dc0-72f9-8e0f-dfa2950a8182"), Title: "AI Research Scientist"},
{ID: uuid.MustParse("0199b964-5dc0-7177-829b-f3d05081201e"), Title: "Computer Vision Engineer"},
{ID: uuid.MustParse("0199b964-5dc0-74b7-b427-a500ddb9f435"), Title: "NLP Engineer"},
{ID: uuid.MustParse("0199b964-5dc0-780d-876f-a7b4d15b0ef5"), Title: "Data Architect"},
{ID: uuid.MustParse("0199b964-5dc0-7d3f-af44-19dc33f50b21"), Title: "Big Data Engineer"},
{ID: uuid.MustParse("0199b964-5dc0-7600-9a16-74f17be7ce4b"), Title: "Cloud AI/ML Specialist"},
{ID: uuid.MustParse("0199b964-5dc0-73c2-b9a0-78347ae945d7"), Title: "Generative AI Specialist"},
{ID: uuid.MustParse("0199b964-5dc0-70a8-b710-1f424a776083"), Title: "AI Ethics Officer"},
{ID: uuid.MustParse("0199b964-5dc0-7c87-91c0-348e6f8b43d6"), Title: "AI Governance Manager"},
{ID: uuid.MustParse("0199b964-5dc0-7441-b306-bc2e3d4e4152"), Title: "Data Privacy Engineer"},
{ID: uuid.MustParse("0199b964-5dc0-747f-97b4-c4d98a257dee"), Title: "AI Solutions Architect"},
{ID: uuid.MustParse("0199b964-5dc0-7fa5-8fe0-9eb7831554ed"), Title: "Chief Data & AI Officer"},
{ID: uuid.MustParse("0199b964-5dc0-7447-8785-f246ff9ec309"), Title: "AI Developer Advocate"},
{ID: uuid.MustParse("0199b964-5dc0-7b24-9b1b-c7ca8f08527f"), Title: "AI/ML Educator & Trainer"},
{ID: uuid.MustParse("0199b964-5dc0-756f-ab44-48169ecfbb5e"), Title: "Technical Content Creator (AI/ML)"},
{ID: uuid.MustParse("0199b964-5dc0-79d1-9086-c809d8989cac"), Title: "Open Source AI Contributor"},
{ID: uuid.MustParse("0199b964-5dc0-774e-9011-b9fe6c29f52f"), Title: "AI Course Instructor (Udemy, Coursera, etc.)"},
{ID: uuid.MustParse("0199b964-5dc0-7f1d-80a4-96810af9f9ac"), Title: "AI Community Manager"},
{ID: uuid.MustParse("0199b964-5dc0-7352-8553-edd37324ffd9"), Title: "AI Evangelist"},
{ID: uuid.MustParse("0199b964-5dc0-7864-a2b5-473cfd8f7aa0"), Title: "Research Engineer (applied AI research, publishing GitHub repos)"},
{ID: uuid.MustParse("0199b964-5dc0-762e-9a40-0cc112578498"), Title: "Kaggle Competitor / Data Science Challenger"},
{ID: uuid.MustParse("0199b964-5dc0-7e13-a1f4-b4ae76bb0b62"), Title: "AI Startup Founder / Indie Hacker (building projects, sharing repos)"},
{ID: uuid.MustParse("0199b964-5dc0-7035-bf9b-deb415d852fd"), Title: "Freelancer"},
{ID: uuid.MustParse("0199b964-5dc0-7702-b533-72f7c93e19d3"), Title: "Other"},
}
// mockRoleRepository returns mocked profile roles (no DB).
type mockRoleRepository struct {
mu sync.RWMutex
data []*domainProfile.Role
}
// NewMockRoleRepository creates a RoleRepository that returns mocked data.
func NewMockRoleRepository() domainProfile.RoleRepository {
data := make([]*domainProfile.Role, len(mockRoleData))
for i, r := range mockRoleData {
data[i] = &domainProfile.Role{ID: r.ID, Title: r.Title}
}
return &mockRoleRepository{data: data}
}
func (r *mockRoleRepository) FindByID(ctx context.Context, id uuid.UUID) (*domainProfile.Role, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, role := range r.data {
if role.ID == id {
return &domainProfile.Role{ID: role.ID, Title: role.Title}, nil
}
}
return nil, domainProfile.ErrRoleNotFound
}
func (r *mockRoleRepository) FindAll(ctx context.Context) ([]*domainProfile.Role, error) {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]*domainProfile.Role, len(r.data))
for i, role := range r.data {
out[i] = &domainProfile.Role{ID: role.ID, Title: role.Title}
}
return out, nil
}
func (r *mockRoleRepository) List(ctx context.Context, limit, offset int) ([]*domainProfile.Role, error) {
r.mu.RLock()
defer r.mu.RUnlock()
start := offset
if start > len(r.data) {
start = len(r.data)
}
end := start + limit
if limit <= 0 {
end = len(r.data)
} else if end > len(r.data) {
end = len(r.data)
}
slice := r.data[start:end]
out := make([]*domainProfile.Role, len(slice))
for i, role := range slice {
out[i] = &domainProfile.Role{ID: role.ID, Title: role.Title}
}
return out, nil
}
func (r *mockRoleRepository) Create(ctx context.Context, role *domainProfile.Role) error {
r.mu.Lock()
defer r.mu.Unlock()
r.data = append(r.data, &domainProfile.Role{ID: role.ID, Title: role.Title})
return nil
}
func (r *mockRoleRepository) Update(ctx context.Context, role *domainProfile.Role) error {
r.mu.Lock()
defer r.mu.Unlock()
for i, existing := range r.data {
if existing.ID == role.ID {
r.data[i] = &domainProfile.Role{ID: role.ID, Title: role.Title}
return nil
}
}
return domainProfile.ErrRoleNotFound
}
func (r *mockRoleRepository) Delete(ctx context.Context, id uuid.UUID) error {
r.mu.Lock()
defer r.mu.Unlock()
for i, role := range r.data {
if role.ID == id {
r.data = append(r.data[:i], r.data[i+1:]...)
return nil
}
}
return domainProfile.ErrRoleNotFound
}

View File

@@ -0,0 +1,106 @@
package profile
import (
"encoding/json"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Model struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
UserID *uuid.UUID `gorm:"column:user_id;type:uuid;index:profiles_user_id_idx"`
Handle string `gorm:"column:handle;type:text;not null;uniqueIndex:profiles_handle_unique"`
// Hero fields (normalized for search)
RoleID *uuid.UUID `gorm:"column:role_id;type:uuid;index:profiles_role_id_idx"`
Role *RoleModel `gorm:"foreignKey:RoleID"`
RoleName *string `gorm:"column:role_name;type:varchar(100)"` // denormalized fallback
RoleLevel string `gorm:"column:role_level;type:text"`
FirstName string `gorm:"column:first_name;type:text;index:profiles_name_idx"`
LastName string `gorm:"column:last_name;type:text;index:profiles_name_idx"`
Company string `gorm:"column:company;type:text;index:profiles_company_idx"`
ShortDescription string `gorm:"column:short_description;type:text"`
ResumeLink string `gorm:"column:resume_link;type:text"`
CTAEnabled bool `gorm:"column:cta_enabled;type:boolean;default:false"`
Avatar string `gorm:"column:avatar;type:text"`
// About fields (normalized for search)
ProfilePicture string `gorm:"column:profile_picture;type:text"`
About string `gorm:"column:about;type:text"`
// Contact fields (normalized for search)
Email string `gorm:"column:email;type:text;index:profiles_email_idx"`
Phone string `gorm:"column:phone;type:text"`
// PageSetting fields (normalized)
VisibilityLevel string `gorm:"column:visibility_level;type:text;default:'public'"`
// Complex/non-searchable data stored as JSONB
PageSectionOrder json.RawMessage `gorm:"column:page_section_order;type:jsonb"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
}
func (Model) TableName() string {
return "profiles"
}
type SkillModel struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
ProfileID uuid.UUID `gorm:"column:profile_id;type:uuid;not null;index:skills_profile_id_idx"`
SkillName string `gorm:"column:skill_name;type:text;not null;index:skills_name_idx"`
Level string `gorm:"column:level;type:text;not null"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
}
func (SkillModel) TableName() string {
return "profile_skills"
}
type SocialLinkModel struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
ProfileID uuid.UUID `gorm:"column:profile_id;type:uuid;not null;index:social_links_profile_id_idx"`
LinkType string `gorm:"column:link_type;type:text;not null"`
Link string `gorm:"column:link;type:text;not null"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
}
func (SocialLinkModel) TableName() string {
return "profile_social_links"
}
type AchievementModel struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"`
ProfileID uuid.UUID `gorm:"column:profile_id;type:uuid;not null;index:achievements_profile_id_idx"`
Title string `gorm:"column:title;type:text;not null"`
Value string `gorm:"column:value;type:text;not null"`
Enabled bool `gorm:"column:enabled;type:boolean;default:true"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
}
func (AchievementModel) TableName() string {
return "profile_achievements"
}
// RoleModel maps profile_roles table (profiles.role_id references this)
type RoleModel struct {
ID uuid.UUID `gorm:"column:id;type:uuid;primaryKey"`
Title string `gorm:"column:title;type:text;not null"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamptz;not null"`
DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamptz;index"`
}
func (RoleModel) TableName() string {
return "profile_roles"
}

View File

@@ -0,0 +1,107 @@
package profile
import (
"testing"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
domainProfile "base/internal/domain/profile"
)
// setupTestDB creates an in-memory SQLite database for testing
func setupTestDB(t *testing.T) *gorm.DB {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
require.NoError(t, err)
// Create tables manually with SQLite-compatible syntax
// This avoids PostgreSQL-specific syntax like gen_random_uuid() and timestamptz
createProfilesTable := `
CREATE TABLE IF NOT EXISTS profiles (
id TEXT PRIMARY KEY,
user_id TEXT,
handle TEXT NOT NULL,
role_id TEXT,
role_name TEXT,
first_name TEXT,
last_name TEXT,
company TEXT,
short_description TEXT,
resume_link TEXT,
cta_enabled INTEGER NOT NULL DEFAULT 0,
avatar TEXT,
profile_picture TEXT,
about TEXT,
email TEXT,
phone TEXT,
visibility_level TEXT NOT NULL DEFAULT 'public',
page_section_order TEXT,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME,
UNIQUE(handle)
)
`
require.NoError(t, db.Exec(createProfilesTable).Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_user_id_idx ON profiles(user_id)").Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_role_id_idx ON profiles(role_id)").Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_name_idx ON profiles(first_name, last_name)").Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_company_idx ON profiles(company)").Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS profiles_email_idx ON profiles(email)").Error)
createProfileSkillsTable := `
CREATE TABLE IF NOT EXISTS profile_skills (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
skill_name TEXT NOT NULL,
level TEXT NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME
)
`
require.NoError(t, db.Exec(createProfileSkillsTable).Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS skills_profile_id_idx ON profile_skills(profile_id)").Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS skills_name_idx ON profile_skills(skill_name)").Error)
createProfileSocialLinksTable := `
CREATE TABLE IF NOT EXISTS profile_social_links (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
link_type TEXT NOT NULL,
link TEXT NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME
)
`
require.NoError(t, db.Exec(createProfileSocialLinksTable).Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS social_links_profile_id_idx ON profile_social_links(profile_id)").Error)
createProfileAchievementsTable := `
CREATE TABLE IF NOT EXISTS profile_achievements (
id TEXT PRIMARY KEY,
profile_id TEXT NOT NULL,
title TEXT NOT NULL,
value TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
deleted_at DATETIME
)
`
require.NoError(t, db.Exec(createProfileAchievementsTable).Error)
require.NoError(t, db.Exec("CREATE INDEX IF NOT EXISTS achievements_profile_id_idx ON profile_achievements(profile_id)").Error)
return db
}
// createTestProfileRepository creates a profile repository for testing
func createTestProfileRepository(db *gorm.DB) domainProfile.Repository {
return &profileRepository{db: db}
}