Open member free-lane for chat initiation; auto-claim on author view; harden lease persist/ownership with readable DOC_BUSY; add notification inbox and supplement-impact HTTP decision APIs wired to composition root.
141 lines
5.8 KiB
Go
141 lines
5.8 KiB
Go
package projectinstance
|
|
|
|
import "testing"
|
|
|
|
func TestRequiredDutyRole(t *testing.T) {
|
|
if requiredDutyRole(DocAccessActionRun) != "" {
|
|
t.Fatal("run is member-level (no duty role)")
|
|
}
|
|
if requiredDutyRole(DocAccessActionEdit) != "" {
|
|
t.Fatal("edit is member-level (no duty role)")
|
|
}
|
|
if requiredDutyRole(DocAccessActionSubmit) != DutyRoleAuthor {
|
|
t.Fatal("submit -> author")
|
|
}
|
|
if requiredDutyRole(DocAccessActionReview) != DutyRoleReviewer {
|
|
t.Fatal("review -> reviewer")
|
|
}
|
|
if requiredDutyRole(DocAccessActionApprove) != DutyRoleApprover {
|
|
t.Fatal("approve -> approver")
|
|
}
|
|
if requiredDutyRole(DocAccessActionView) != "" {
|
|
t.Fatal("view has no duty role")
|
|
}
|
|
}
|
|
|
|
func TestClassifyAccess_FullDutyMatrix(t *testing.T) {
|
|
dt := DocTaskPlan{Enabled: true, Duties: DocDuty{AuthorUserID: 1, ReviewerUserID: 2, ApproverUserID: 3}}
|
|
creatorID := int64(9)
|
|
|
|
cases := []struct {
|
|
name string
|
|
userID int64
|
|
isCreator bool
|
|
action string
|
|
wantAllow bool
|
|
wantCode string
|
|
}{
|
|
{"author run", 1, false, DocAccessActionRun, true, ""},
|
|
{"author edit", 1, false, DocAccessActionEdit, true, ""},
|
|
{"author submit", 1, false, DocAccessActionSubmit, true, ""},
|
|
{"author review denied", 1, false, DocAccessActionReview, false, AccessCodeForbiddenDuty},
|
|
{"author approve denied", 1, false, DocAccessActionApprove, false, AccessCodeForbiddenDuty},
|
|
{"reviewer run allowed", 2, false, DocAccessActionRun, true, ""},
|
|
{"reviewer edit allowed", 2, false, DocAccessActionEdit, true, ""},
|
|
{"reviewer submit denied", 2, false, DocAccessActionSubmit, false, AccessCodeForbiddenDuty},
|
|
{"reviewer review", 2, false, DocAccessActionReview, true, ""},
|
|
{"approver run allowed", 3, false, DocAccessActionRun, true, ""},
|
|
{"approver edit allowed", 3, false, DocAccessActionEdit, true, ""},
|
|
{"approver review denied", 3, false, DocAccessActionReview, false, AccessCodeForbiddenDuty},
|
|
{"approver approve", 3, false, DocAccessActionApprove, true, ""},
|
|
{"creator view", creatorID, true, DocAccessActionView, true, ""},
|
|
{"member run allowed", creatorID, true, DocAccessActionRun, true, ""},
|
|
{"member edit allowed", creatorID, true, DocAccessActionEdit, true, ""},
|
|
{"creator submit denied", creatorID, true, DocAccessActionSubmit, false, AccessCodeForbiddenDuty},
|
|
{"creator review denied", creatorID, true, DocAccessActionReview, false, AccessCodeForbiddenDuty},
|
|
{"creator approve denied", creatorID, true, DocAccessActionApprove, false, AccessCodeForbiddenDuty},
|
|
{"reviewer can re-run after reject", 2, false, DocAccessActionRun, true, ""},
|
|
}
|
|
for _, tc := range cases {
|
|
got := classifyAccess(dt, tc.action, tc.userID, tc.isCreator, true)
|
|
if got.Allowed != tc.wantAllow || (tc.wantCode != "" && got.Code != tc.wantCode) {
|
|
t.Fatalf("%s: got %+v want allow=%v code=%s", tc.name, got, tc.wantAllow, tc.wantCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClassifyAccess_NoneAndPreActivation(t *testing.T) {
|
|
none := DocTaskPlan{Enabled: true, Duties: DocDuty{}}
|
|
creatorRun := classifyAccess(none, DocAccessActionRun, 1, true, true)
|
|
if !creatorRun.Allowed {
|
|
t.Fatalf("creator run on none: %+v", creatorRun)
|
|
}
|
|
creatorEdit := classifyAccess(none, DocAccessActionEdit, 1, true, true)
|
|
if !creatorEdit.Allowed {
|
|
t.Fatalf("creator edit on none: %+v", creatorEdit)
|
|
}
|
|
memberRun := classifyAccess(none, DocAccessActionRun, 2, false, true)
|
|
if !memberRun.Allowed {
|
|
t.Fatalf("member run on none must allow: %+v", memberRun)
|
|
}
|
|
memberEdit := classifyAccess(none, DocAccessActionEdit, 2, false, true)
|
|
if !memberEdit.Allowed {
|
|
t.Fatalf("member edit on none must allow: %+v", memberEdit)
|
|
}
|
|
submit := classifyAccess(none, DocAccessActionSubmit, 1, true, true)
|
|
if submit.Allowed || submit.Code != AccessCodeDutyNotManaged {
|
|
t.Fatalf("submit on none: %+v", submit)
|
|
}
|
|
preRun := classifyAccess(none, DocAccessActionRun, 1, true, false)
|
|
if !preRun.Allowed || preRun.Code != AccessCodeNotActivated {
|
|
t.Fatalf("pre-activation run: %+v", preRun)
|
|
}
|
|
preSubmit := classifyAccess(none, DocAccessActionSubmit, 1, true, false)
|
|
if preSubmit.Allowed || preSubmit.Code != AccessCodeNotActivated {
|
|
t.Fatalf("pre-activation submit: %+v", preSubmit)
|
|
}
|
|
}
|
|
|
|
func TestClassifyAccess_PartialStillDeniesRunEdit(t *testing.T) {
|
|
partial := DocTaskPlan{Enabled: true, Duties: DocDuty{AuthorUserID: 1}}
|
|
for _, action := range []string{DocAccessActionRun, DocAccessActionEdit, DocAccessActionSubmit} {
|
|
got := classifyAccess(partial, action, 1, true, true)
|
|
if got.Allowed || got.Code != AccessCodeForbiddenDuty {
|
|
t.Fatalf("partial %s must deny: %+v", action, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestClassifyAccess_AuthorOnlyMatrix(t *testing.T) {
|
|
dt := DocTaskPlan{
|
|
Enabled: true,
|
|
DutyPolicy: DutyPolicyAuthorOnly,
|
|
Duties: DocDuty{AuthorUserID: 7},
|
|
}
|
|
cases := []struct {
|
|
name string
|
|
userID int64
|
|
action string
|
|
wantAllow bool
|
|
wantCode string
|
|
}{
|
|
{"author run", 7, DocAccessActionRun, true, ""},
|
|
{"author edit", 7, DocAccessActionEdit, true, ""},
|
|
{"author submit", 7, DocAccessActionSubmit, true, ""},
|
|
{"author review", 7, DocAccessActionReview, true, ""},
|
|
{"author approve", 7, DocAccessActionApprove, true, ""},
|
|
{"other run allowed", 2, DocAccessActionRun, true, ""},
|
|
{"other edit allowed", 2, DocAccessActionEdit, true, ""},
|
|
{"other submit denied", 2, DocAccessActionSubmit, false, AccessCodeForbiddenDuty},
|
|
{"other review denied", 2, DocAccessActionReview, false, AccessCodeForbiddenDuty},
|
|
{"other approve denied", 3, DocAccessActionApprove, false, AccessCodeForbiddenDuty},
|
|
{"creator cannot elevate approve", 9, DocAccessActionApprove, false, AccessCodeForbiddenDuty},
|
|
}
|
|
for _, tc := range cases {
|
|
got := classifyAccess(dt, tc.action, tc.userID, tc.userID == 9, true)
|
|
if got.Allowed != tc.wantAllow || (tc.wantCode != "" && got.Code != tc.wantCode) {
|
|
t.Fatalf("%s: got %+v want allow=%v code=%s", tc.name, got, tc.wantAllow, tc.wantCode)
|
|
}
|
|
}
|
|
}
|