Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion controllers/corp_signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package controllers
import (
"strings"

"github.com/beego/beego/v2/core/logs"

"github.com/opensourceways/app-cla-server/models"
"github.com/opensourceways/app-cla-server/signing/domain/dp"
"github.com/opensourceways/app-cla-server/worker"
Expand Down Expand Up @@ -365,10 +367,20 @@ func (ctl *CorporationSigningController) UpdateRepresentative() {
return
}

if merr := models.UpdateCorpRepresentative(pl.UserId, linkID, signingID, &opt); merr != nil {
if created, merr := models.UpdateCorpRepresentative(pl.UserId, linkID, signingID, &opt); merr != nil {
ctl.sendModelErrorAsResp(merr, action)
} else {
ctl.sendSuccessResp(action, "representative updated successfully")

// 本次为新管理员补建了账号:发送账号通知邮件(best-effort)
if created != nil {
orgInfo, omerr := models.GetLink(linkID)
if omerr != nil {
logs.Error("get link info failed after updating representative, link_id: %s, err: %s", linkID, omerr.Error())
} else {
notifyCorpAdmin(linkID, &orgInfo, created)
}
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion models/corp_signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ type RepresentativeUpdateOption struct {
RepEmail string `json:"rep_email" valid:"Required;Email"`
}

func UpdateCorpRepresentative(userId, linkID, signingID string, opt *RepresentativeUpdateOption) IModelError {
func UpdateCorpRepresentative(userId, linkID, signingID string, opt *RepresentativeUpdateOption) (*CorporationManagerCreateOption, IModelError) {
return corpSigningAdapterInstance.UpdateRepresentative(userId, linkID, signingID, opt)
}

Expand Down
2 changes: 1 addition & 1 deletion models/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type corpSigningAdapter interface {
FindCorpSummary(linkId string, email string) (interface{}, IModelError)
FindDiffCLAFile(signingId string) (string, IModelError)
Agree(signingId string) IModelError
UpdateRepresentative(userId, linkID, signingID string, opt *RepresentativeUpdateOption) IModelError
UpdateRepresentative(userId, linkID, signingID string, opt *RepresentativeUpdateOption) (*CorporationManagerCreateOption, IModelError)
FindPendingAgreements(userId, linkId string) ([]CorporationSigningPendingItem, IModelError)
}

Expand Down
17 changes: 11 additions & 6 deletions signing/adapter/corp_signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,20 +272,25 @@ func getAllSigningInfo(
return r, nil
}

func (adapter *corpSigningAdatper) UpdateRepresentative(userId, linkID, signingID string, opt *models.RepresentativeUpdateOption) models.IModelError {
func (adapter *corpSigningAdatper) UpdateRepresentative(userId, linkID, signingID string, opt *models.RepresentativeUpdateOption) (*models.CorporationManagerCreateOption, models.IModelError) {
if opt.RepName == "" || opt.RepEmail == "" {
return toModelError(errors.New("representative name and email are required"))
return nil, toModelError(errors.New("representative name and email are required"))
}

if _, err := dp.NewEmailAddr(opt.RepEmail); err != nil {
return toModelError(err)
return nil, toModelError(err)
}

err := adapter.s.UpdateRepresentative(userId, linkID, signingID, opt.RepName, opt.RepEmail)
created, err := adapter.s.UpdateRepresentative(userId, linkID, signingID, opt.RepName, opt.RepEmail)
if err != nil {
return toModelError(err)
return nil, toModelError(err)
}
return nil
if created == nil {
return nil, nil
}

r := toCorporationManagerCreateOption(created)
return &r, nil
}

func (adapter *corpSigningAdatper) FindPendingAgreements(userId, linkId string) ([]models.CorporationSigningPendingItem, models.IModelError) {
Expand Down
133 changes: 133 additions & 0 deletions signing/adapter/corp_signing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package adapter

import (
"errors"
"testing"

"github.com/opensourceways/app-cla-server/models"
"github.com/opensourceways/app-cla-server/signing/app"
"github.com/opensourceways/app-cla-server/signing/domain"
)

// ---- mock ----

type mockCorpSigningService struct {
app.CorpSigningService // embed nil interface

created *app.ManagerDTO
err error

calledRep bool
repName string
repEmail string
}

func (m *mockCorpSigningService) UpdateRepresentative(userId, linkID, signingID, repName, repEmail string) (*app.ManagerDTO, error) {
m.calledRep = true
m.repName = repName
m.repEmail = repEmail
return m.created, m.err
}

// ---- tests ----

func TestUpdateRepresentative_Validation_EmptyName(t *testing.T) {
a := &corpSigningAdatper{s: &mockCorpSigningService{}}
opt := &models.RepresentativeUpdateOption{RepName: "", RepEmail: "b@corp.com"}

r, merr := a.UpdateRepresentative("u", "l", "s", opt)
if merr == nil {
t.Fatal("expected model error for empty name")
}
if r != nil {
t.Errorf("result should be nil, got %+v", r)
}
}

func TestUpdateRepresentative_Validation_EmptyEmail(t *testing.T) {
a := &corpSigningAdatper{s: &mockCorpSigningService{}}
opt := &models.RepresentativeUpdateOption{RepName: "Bob", RepEmail: ""}

r, merr := a.UpdateRepresentative("u", "l", "s", opt)
if merr == nil {
t.Fatal("expected model error for empty email")
}
if r != nil {
t.Errorf("result should be nil, got %+v", r)
}
}

func TestUpdateRepresentative_Validation_InvalidEmail(t *testing.T) {
a := &corpSigningAdatper{s: &mockCorpSigningService{}}
opt := &models.RepresentativeUpdateOption{RepName: "Bob", RepEmail: "not-an-email"}

r, merr := a.UpdateRepresentative("u", "l", "s", opt)
if merr == nil {
t.Fatal("expected model error for invalid email")
}
if r != nil {
t.Errorf("result should be nil, got %+v", r)
}
}

func TestUpdateRepresentative_NoNewAccount(t *testing.T) {
svc := &mockCorpSigningService{created: nil}
a := &corpSigningAdatper{s: svc}
opt := &models.RepresentativeUpdateOption{RepName: "Bob", RepEmail: "b@corp.com"}

r, merr := a.UpdateRepresentative("u", "l", "s", opt)
if merr != nil {
t.Fatalf("unexpected error: %v", merr)
}
if r != nil {
t.Errorf("result should be nil when no account created, got %+v", r)
}
if !svc.calledRep {
t.Error("service UpdateRepresentative should be called")
}
if svc.repName != "Bob" || svc.repEmail != "b@corp.com" {
t.Errorf("args: name=%s email=%s", svc.repName, svc.repEmail)
}
}

func TestUpdateRepresentative_NewAccountCreated(t *testing.T) {
created := &app.ManagerDTO{
Role: domain.RoleAdmin,
Name: "Bob",
Account: "admin_corp.com",
Password: []byte("init-pw"),
EmailAddr: "b@corp.com",
}
svc := &mockCorpSigningService{created: created}
a := &corpSigningAdatper{s: svc}
opt := &models.RepresentativeUpdateOption{RepName: "Bob", RepEmail: "b@corp.com"}

r, merr := a.UpdateRepresentative("u", "l", "s", opt)
if merr != nil {
t.Fatalf("unexpected error: %v", merr)
}
if r == nil {
t.Fatal("result should not be nil when account created")
}
if r.ID != "admin_corp.com" || r.Email != "b@corp.com" || r.Role != domain.RoleAdmin {
t.Errorf("mapped result: %+v", r)
}
if string(r.Password) != "init-pw" {
t.Errorf("password: got %s", string(r.Password))
}
}

func TestUpdateRepresentative_ServiceError(t *testing.T) {
wantErr := errors.New("boom")
svc := &mockCorpSigningService{err: wantErr}
a := &corpSigningAdatper{s: svc}
opt := &models.RepresentativeUpdateOption{RepName: "Bob", RepEmail: "b@corp.com"}

r, merr := a.UpdateRepresentative("u", "l", "s", opt)
if merr == nil {
t.Fatal("expected model error")
}
if r != nil {
t.Errorf("result should be nil on error, got %+v", r)
}
}
20 changes: 20 additions & 0 deletions signing/adapter/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package adapter

import (
"os"
"testing"

"github.com/opensourceways/app-cla-server/signing/domain/dp"
)

func TestMain(m *testing.M) {
dp.Init(&dp.Config{
MaxLengthOfName: 100,
MaxLengthOfTitle: 200,
MaxLengthOfEmail: 200,
MaxLengthOfAccount: 100,
MaxLengthOfCorpName: 200,
})

os.Exit(m.Run())
}
76 changes: 65 additions & 11 deletions signing/app/corp_signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"errors"
"time"

"github.com/beego/beego/v2/core/logs"

commonRepo "github.com/opensourceways/app-cla-server/common/domain/repository"
"github.com/opensourceways/app-cla-server/signing/domain"
"github.com/opensourceways/app-cla-server/signing/domain/claservice"
Expand Down Expand Up @@ -41,7 +43,7 @@ type CorpSigningService interface {
FindCorpSummary(cmd *CmdToFindCorpSummary) ([]CorpSummaryDTO, error)
FindDiffCLAFile(signingId string) (string, error)
AgreeWithLatestCLA(signingId string) error
UpdateRepresentative(userId, linkID, signingID, repName, repEmail string) error
UpdateRepresentative(userId, linkID, signingID, repName, repEmail string) (*ManagerDTO, error)
FindPendingAgreements(userId, linkId string) ([]CorpSigningPendingDTO, error)
}

Expand Down Expand Up @@ -248,27 +250,27 @@ func (s *corpSigningService) AgreeWithLatestCLA(signingId string) error {
return s.repo.UpdateClaId(&signed)
}

func (s *corpSigningService) UpdateRepresentative(userId, linkID, signingID, repName, repEmail string) error {
func (s *corpSigningService) UpdateRepresentative(userId, linkID, signingID, repName, repEmail string) (*ManagerDTO, error) {
// 权限验证 - 只有社区管理员可以操作
if _, err := checkIfCommunityManager(userId, linkID, s.linkRepo); err != nil {
return err
return nil, err
}

// 查找企业签名
cs, err := s.repo.Find(signingID)
if err != nil {
return err
return nil, err
}

// 验证link_id匹配
if cs.Link.Id != linkID {
return commonRepo.NewErrorResourceNotFound(errors.New("signing not found"))
return nil, commonRepo.NewErrorResourceNotFound(errors.New("signing not found"))
}

// 创建新的代表信息
newRep, err := domain.NewRepresentative(repName, repEmail)
if err != nil {
return err
return nil, err
}

oldEmail := cs.Rep.EmailAddr
Expand All @@ -283,17 +285,69 @@ func (s *corpSigningService) UpdateRepresentative(userId, linkID, signingID, rep

// 保存到数据库
if err := s.repo.Update(&cs); err != nil {
return err
return nil, err
}

// 同步更新 User 表中的邮箱
if cs.Admin.Id != "" {
// 无管理员账号或邮箱未变:无需同步 user 表
if cs.Admin.Id == "" || oldEmail.EmailAddr() == newRep.EmailAddr.EmailAddr() {
return nil, nil
}

// 旧邮箱对应账号已存在:复用并改写邮箱
exists, err := s.userService.IsAValidUser(cs.Link.Id, oldEmail)
if err != nil {
return nil, err
}
if exists {
if err := s.userService.UpdateEmail(cs.Link.Id, oldEmail, newRep.EmailAddr); err != nil {
return err
return nil, err
}
return nil, nil
}

// 旧账号缺失(未预置/迁移遗留):为当前管理员新建账号
logs.Info(
"create user account for the new representative, since the old email(%s) is not found, link_id: %s, signing_id: %s",
oldEmail.EmailAddr(), cs.Link.Id, signingID,
)

pws, _, err := s.userService.Add(cs.Link.Id, signingID, []domain.Manager{cs.Admin})
if err == nil {
account, aerr := cs.Admin.Account()
if aerr != nil {
return nil, aerr
}

admin := &cs.Admin
return &ManagerDTO{
Role: domain.RoleAdmin,
Name: admin.Name.Name(),
Account: account.Account(),
Password: pws[admin.Id].Password(),
EmailAddr: admin.EmailAddr.EmailAddr(),
}, nil
}

// 新建失败且为账号重复(存在邮箱漂移的孤儿管理员账号):
// 按新账号定位孤儿并改写其邮箱,复用已有账号
if domain.IsErrorOf(err, domain.ErrorCodeUserExists) {
account, aerr := cs.Admin.Account()
if aerr != nil {
return nil, aerr
}

logs.Info(
"fallback to update email by account(%s) for orphan admin, link_id: %s",
account.Account(), cs.Link.Id,
)

if uerr := s.userService.UpdateEmailByAccount(cs.Link.Id, account, newRep.EmailAddr); uerr != nil {
return nil, uerr
}
return nil, nil
}

return nil
return nil, err
}

func (s *corpSigningService) FindPendingAgreements(userId, linkId string) ([]CorpSigningPendingDTO, error) {
Expand Down
Loading
Loading