initial commit
This commit is contained in:
196
internal/repository/postgres/profile/mapper.go
Normal file
196
internal/repository/postgres/profile/mapper.go
Normal 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
|
||||
}
|
||||
315
internal/repository/postgres/profile/profile.go
Normal file
315
internal/repository/postgres/profile/profile.go
Normal 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
|
||||
}
|
||||
870
internal/repository/postgres/profile/profile_test.go
Normal file
870
internal/repository/postgres/profile/profile_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
|
||||
112
internal/repository/postgres/profile/role.go
Normal file
112
internal/repository/postgres/profile/role.go
Normal 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
|
||||
}
|
||||
134
internal/repository/postgres/profile/role_mock.go
Normal file
134
internal/repository/postgres/profile/role_mock.go
Normal 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
|
||||
}
|
||||
106
internal/repository/postgres/profile/schema.go
Normal file
106
internal/repository/postgres/profile/schema.go
Normal 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"
|
||||
}
|
||||
107
internal/repository/postgres/profile/test_helper.go
Normal file
107
internal/repository/postgres/profile/test_helper.go
Normal 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}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user