diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index bb4185f785..e1ee785ce4 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -604,7 +604,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { repoOwnerType = project_model.TypeOrganization } var err error - projects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ + repositoryProjects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ ListOptions: db.ListOptionsAll, RepoID: repo.ID, IsClosed: optional.Some(false), @@ -614,7 +614,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { ctx.ServerError("GetProjects", err) return } - projects2, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ + ownerProjects, err := db.Find[project_model.Project](ctx, project_model.SearchOptions{ ListOptions: db.ListOptionsAll, OwnerID: repo.OwnerID, IsClosed: optional.Some(false), @@ -625,9 +625,10 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { return } - ctx.Data["OpenProjects"] = append(projects, projects2...) + ownerHasOpenProjects := len(ownerProjects) > 0 + ctx.Data["OpenProjects"] = append(repositoryProjects, ownerProjects...) - projects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ + repositoryProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ ListOptions: db.ListOptionsAll, RepoID: repo.ID, IsClosed: optional.Some(true), @@ -637,7 +638,7 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { ctx.ServerError("GetProjects", err) return } - projects2, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ + ownerProjects, err = db.Find[project_model.Project](ctx, project_model.SearchOptions{ ListOptions: db.ListOptionsAll, OwnerID: repo.OwnerID, IsClosed: optional.Some(true), @@ -648,7 +649,8 @@ func retrieveProjects(ctx *context.Context, repo *repo_model.Repository) { return } - ctx.Data["ClosedProjects"] = append(projects, projects2...) + ctx.Data["OwnerHasProjects"] = ownerHasOpenProjects || len(ownerProjects) > 0 + ctx.Data["ClosedProjects"] = append(repositoryProjects, ownerProjects...) } // repoReviewerSelection items to bee shown @@ -968,6 +970,14 @@ func NewIssue(ctx *context.Context) { isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects) ctx.Data["IsProjectsEnabled"] = isProjectsEnabled + + // Individuals always have projects unit enabled + isOwnerProjectsEnabled := true + if ctx.Repo.Owner.IsOrganization() { + isOwnerProjectsEnabled = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) + } + ctx.Data["IsOwnerProjectsEnabled"] = isOwnerProjectsEnabled + ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -983,7 +993,7 @@ func NewIssue(ctx *context.Context) { } projectID := ctx.FormInt64("project") - if projectID > 0 && isProjectsEnabled { + if projectID > 0 && (isProjectsEnabled || isOwnerProjectsEnabled) { project, err := project_model.GetProjectByID(ctx, projectID) if err != nil { log.Error("GetProjectByID: %d: %v", projectID, err) @@ -1276,9 +1286,9 @@ func NewIssuePost(ctx *context.Context) { } if projectID > 0 { - if !ctx.Repo.CanRead(unit.TypeProjects) { + if !ctx.Repo.CanRead(unit.TypeProjects) || (ctx.ContextUser.IsOrganization() && !ctx.Org.CanReadUnit(ctx, unit.TypeProjects)) { // User must also be able to see the project. - ctx.Error(http.StatusBadRequest, "user hasn't permissions to read projects") + ctx.Error(http.StatusForbidden, "user doesn't have permissions to read projects") return } if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { @@ -1477,7 +1487,8 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["IsModerationEnabled"] = setting.Moderation.Enabled - ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects) + isProjectsEnabled := ctx.Repo.CanRead(unit.TypeProjects) + ctx.Data["IsProjectsEnabled"] = isProjectsEnabled ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled upload.AddUploadContext(ctx, "comment") @@ -1546,6 +1557,7 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["Labels"] = labels + isOwnerProjectsEnabled := true if repo.Owner.IsOrganization() { orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}) if err != nil { @@ -1553,9 +1565,11 @@ func ViewIssue(ctx *context.Context) { return } ctx.Data["OrgLabels"] = orgLabels - labels = append(labels, orgLabels...) + + isOwnerProjectsEnabled = ctx.Org.CanReadUnit(ctx, unit.TypeProjects) } + ctx.Data["IsOwnerProjectsEnabled"] = isOwnerProjectsEnabled hasSelected := false for i := range labels { @@ -1569,7 +1583,9 @@ func ViewIssue(ctx *context.Context) { // Check milestone and assignee. if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) { RetrieveRepoMilestonesAndAssignees(ctx, repo) - retrieveProjects(ctx, repo) + if isProjectsEnabled || isOwnerProjectsEnabled { + retrieveProjects(ctx, repo) + } if ctx.Written() { return diff --git a/routers/web/repo/issue_test.go b/routers/web/repo/issue_test.go index 3644dd131f..e2c812084d 100644 --- a/routers/web/repo/issue_test.go +++ b/routers/web/repo/issue_test.go @@ -74,6 +74,9 @@ func TestNewIssueValidateProject(t *testing.T) { contexttest.LoadUser(t, ctx, testCase.userID) contexttest.LoadRepo(t, ctx, testCase.repoID) contexttest.LoadGitRepo(t, ctx) + if ctx.Repo.Owner.IsOrganization() { + contexttest.LoadOrganization(t, ctx, ctx.Repo.Owner.ID) + } NewIssue(ctx) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index 98e4c35fa2..f007dfa7ba 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -400,7 +400,7 @@ func UpdateIssueProject(ctx *context.Context) { return } if _, err := issues.LoadRepositories(ctx); err != nil { - ctx.ServerError("LoadProjects", err) + ctx.ServerError("LoadRepositories", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index 5839e7d30e..c77c8629b9 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -906,6 +906,29 @@ func registerRoutes(m *web.Route) { } } + reqRepoOrOwnerProjectReader := func(ctx *context.Context) { + unitType := unit.TypeProjects + if ctx.ContextUser == nil || ctx.Doer == nil { + ctx.NotFound(unitType.String(), nil) + return + } + + switch { + case ctx.ContextUser.IsIndividual(): + if ctx.Doer.ID == ctx.ContextUser.ID || ctx.Doer.IsAdmin { + return + } + case ctx.ContextUser.IsOrganization(): + if ctx.Org.Organization.UnitPermission(ctx, ctx.Doer, unitType) >= perm.AccessModeRead { + return + } + default: + ctx.NotFound(unitType.String(), nil) + return + } + reqRepoProjectsReader(ctx) + } + individualPermsChecker := func(ctx *context.Context) { // org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked. if ctx.ContextUser.IsIndividual() { @@ -1255,7 +1278,7 @@ func registerRoutes(m *web.Route) { } m.Group("/issues", func() { m.Group("/new", func() { - m.Combo("").Get(context.RepoRef(), repo.NewIssue). + m.Combo("", context.EnsureOrg()).Get(context.RepoRef(), repo.NewIssue). Post(web.Bind(forms.CreateIssueForm{}), repo.NewIssuePost) m.Get("/choose", context.RepoRef(), repo.NewIssueChooseTemplate) }) @@ -1301,7 +1324,7 @@ func registerRoutes(m *web.Route) { m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) - m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject) + m.Post("/projects", reqRepoIssuesOrPullsWriter, context.EnsureOrg(), reqRepoOrOwnerProjectReader, repo.UpdateIssueProject) m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee) m.Post("/request_review", reqRepoIssuesOrPullsReader, repo.UpdatePullReviewRequest) m.Post("/dismiss_review", reqRepoAdmin, web.Bind(forms.DismissReviewForm{}), repo.DismissReview) @@ -1427,7 +1450,7 @@ func registerRoutes(m *web.Route) { m.Group("", func() { m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because other routes like "/pulls/{index}" has higher priority m.Get("/{type:^(issues|pulls)$}", repo.Issues) - m.Get("/{type:^(issues|pulls)$}/{index}", repo.ViewIssue) + m.Get("/{type:^(issues|pulls)$}/{index}", context.EnsureOrg(), repo.ViewIssue) m.Group("/{type:^(issues|pulls)$}/{index}/content-history", func() { m.Get("/overview", repo.GetContentHistoryOverview) m.Get("/list", repo.GetContentHistoryList) @@ -1600,7 +1623,7 @@ func registerRoutes(m *web.Route) { m.Get("/pulls/posters", repo.PullPosters) m.Group("/pulls/{index}", func() { - m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, repo.ViewIssue) + m.Get("", repo.SetWhitespaceBehavior, repo.GetPullDiffStats, context.EnsureOrg(), repo.ViewIssue) m.Get(".diff", repo.DownloadPullDiff) m.Get(".patch", repo.DownloadPullPatch) m.Group("/commits", func() { diff --git a/services/context/org.go b/services/context/org.go index c7d06b9bcc..adc6e33494 100644 --- a/services/context/org.go +++ b/services/context/org.go @@ -64,6 +64,32 @@ func GetOrganizationByParams(ctx *Context) { } } +func ensureIsOrg(ctx *Context) bool { + switch { + // Getting Organization from params + case ctx.ContextUser == nil: + if ctx.Org.Organization == nil { + GetOrganizationByParams(ctx) + } + return !ctx.Written() + // Getting Organization from ContextUser + case ctx.ContextUser.IsOrganization(): + if ctx.Org == nil { + ctx.Org = &Organization{} + } + ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) + return true + } + // ContextUser is an individual User + return false +} + +func EnsureOrg() func(*Context) { + return func(ctx *Context) { + ensureIsOrg(ctx) + } +} + // HandleOrgAssignment handles organization assignment func HandleOrgAssignment(ctx *Context, args ...bool) { var ( @@ -87,24 +113,9 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { var err error - if ctx.ContextUser == nil { - // if Organization is not defined, get it from params - if ctx.Org.Organization == nil { - GetOrganizationByParams(ctx) - if ctx.Written() { - return - } - } - } else if ctx.ContextUser.IsOrganization() { - if ctx.Org == nil { - ctx.Org = &Organization{} - } - ctx.Org.Organization = (*organization.Organization)(ctx.ContextUser) - } else { - // ContextUser is an individual User + if !ensureIsOrg(ctx) { return } - org := ctx.Org.Organization // Handle Visibility diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl index 10a5978d52..5f5820fb07 100644 --- a/templates/repo/issue/new_form.tmpl +++ b/templates/repo/issue/new_form.tmpl @@ -78,7 +78,7 @@ - {{if .IsProjectsEnabled}} + {{if or .IsProjectsEnabled (and .OwnerHasProjects .IsOwnerProjectsEnabled)}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl index 72e214c5aa..9d8db413fa 100644 --- a/templates/repo/issue/view_content/sidebar.tmpl +++ b/templates/repo/issue/view_content/sidebar.tmpl @@ -14,8 +14,10 @@ {{template "repo/issue/view_content/sidebar/milestones" .}}
- {{template "repo/issue/view_content/sidebar/projects" .}} -
+ {{if or .IsProjectsEnabled (and .OwnerHasProjects .IsOwnerProjectsEnabled)}} + {{template "repo/issue/view_content/sidebar/projects" .}} +
+ {{end}} {{template "repo/issue/view_content/sidebar/assignees" dict "isExistingIssue" true "." .}}
diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/HEAD b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/HEAD new file mode 100644 index 0000000000..cb089cd89a --- /dev/null +++ b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/config b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/config new file mode 100644 index 0000000000..e6da231579 --- /dev/null +++ b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + ignorecase = true + precomposeunicode = true diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/description b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/description new file mode 100644 index 0000000000..498b267a8c --- /dev/null +++ b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/info/exclude b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/info/exclude new file mode 100644 index 0000000000..a5196d1be8 --- /dev/null +++ b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/20/ade30d25e0ecaeec84e7f542a8456900858240 b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/20/ade30d25e0ecaeec84e7f542a8456900858240 new file mode 100644 index 0000000000..9f3ffe5f27 Binary files /dev/null and b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/20/ade30d25e0ecaeec84e7f542a8456900858240 differ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/27/74debeea6dc742cc4971a92db0e08b95b60588 b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/27/74debeea6dc742cc4971a92db0e08b95b60588 new file mode 100644 index 0000000000..5d9226f7a1 Binary files /dev/null and b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/27/74debeea6dc742cc4971a92db0e08b95b60588 differ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/2a/47ca4b614a9f5a43abbd5ad851a54a616ffee6 b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/2a/47ca4b614a9f5a43abbd5ad851a54a616ffee6 new file mode 100644 index 0000000000..ca60d2314f Binary files /dev/null and b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/2a/47ca4b614a9f5a43abbd5ad851a54a616ffee6 differ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/2f/9b22fd3159a43b7b4e5dd806fcd544edf8716f b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/2f/9b22fd3159a43b7b4e5dd806fcd544edf8716f new file mode 100644 index 0000000000..e98d752dad Binary files /dev/null and b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/2f/9b22fd3159a43b7b4e5dd806fcd544edf8716f differ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/d2/2b4d4daa5be07329fcef6ed458f00cf3392da0 b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/d2/2b4d4daa5be07329fcef6ed458f00cf3392da0 new file mode 100644 index 0000000000..e319f8ce34 Binary files /dev/null and b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/d2/2b4d4daa5be07329fcef6ed458f00cf3392da0 differ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/d5/6a3073c1dbb7b15963110a049d50cdb5db99fc b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/d5/6a3073c1dbb7b15963110a049d50cdb5db99fc new file mode 100644 index 0000000000..eff3c9833e Binary files /dev/null and b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/d5/6a3073c1dbb7b15963110a049d50cdb5db99fc differ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/ec/f0db3c1ec806522de4b491fb9a3c7457398c61 b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/ec/f0db3c1ec806522de4b491fb9a3c7457398c61 new file mode 100644 index 0000000000..ed431f70d3 Binary files /dev/null and b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/ec/f0db3c1ec806522de4b491fb9a3c7457398c61 differ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/ee/16d127df463aa491e08958120f2108b02468df b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/ee/16d127df463aa491e08958120f2108b02468df new file mode 100644 index 0000000000..e177f69e37 Binary files /dev/null and b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/objects/ee/16d127df463aa491e08958120f2108b02468df differ diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/refs/heads/master b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/refs/heads/master new file mode 100644 index 0000000000..ccee722d52 --- /dev/null +++ b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/refs/heads/master @@ -0,0 +1 @@ +2a47ca4b614a9f5a43abbd5ad851a54a616ffee6 diff --git a/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/refs/heads/test_branch b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/refs/heads/test_branch new file mode 100644 index 0000000000..dfe0c6a128 --- /dev/null +++ b/tests/gitea-repositories-meta/test_assign_project_org/test_assign_project_repo.git/refs/heads/test_branch @@ -0,0 +1 @@ +d22b4d4daa5be07329fcef6ed458f00cf3392da0 diff --git a/tests/integration/fixtures/TestAssignProject/access.yml b/tests/integration/fixtures/TestAssignProject/access.yml new file mode 100644 index 0000000000..9dec50f0ac --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/access.yml @@ -0,0 +1,5 @@ +- + id: 1001 + user_id: 5 + repo_id: 1001 + mode: 2 diff --git a/tests/integration/fixtures/TestAssignProject/collaboration.yml b/tests/integration/fixtures/TestAssignProject/collaboration.yml new file mode 100644 index 0000000000..a26149d60e --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/collaboration.yml @@ -0,0 +1,5 @@ +- + id: 1001 + repo_id: 1001 + user_id: 5 + mode: 2 diff --git a/tests/integration/fixtures/TestAssignProject/project.yml b/tests/integration/fixtures/TestAssignProject/project.yml new file mode 100644 index 0000000000..20b75ddb69 --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/project.yml @@ -0,0 +1,51 @@ +# team 1001 projects +- + id: 1001 + title: project1001 + owner_id: 3 + repo_id: 0 + is_closed: false + board_type: 0 + type: 3 + creator_id: 5 + created_unix: 1688973030 + updated_unix: 1688973030 + +# team 1002 projects +- + id: 1002 + title: project1002 + owner_id: 0 + repo_id: 1001 + is_closed: false + board_type: 0 + type: 2 + creator_id: 5 + created_unix: 1688973030 + updated_unix: 1688973030 + +# user 5 project +- + id: 1003 + title: project1003 + owner_id: 5 + repo_id: 0 + is_closed: false + board_type: 0 + type: 1 + creator_id: 5 + created_unix: 1688973030 + updated_unix: 1688973030 + +# org 1001 disabled project +- + id: 1004 + title: project1004 + owner_id: 1001 + repo_id: 0 + is_closed: false + board_type: 0 + type: 3 + creator_id: 5 + created_unix: 1688973030 + updated_unix: 1688973030 diff --git a/tests/integration/fixtures/TestAssignProject/repo_unit.yml b/tests/integration/fixtures/TestAssignProject/repo_unit.yml new file mode 100644 index 0000000000..d3fad496aa --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/repo_unit.yml @@ -0,0 +1,35 @@ +- + id: 1001 + repo_id: 3 + type: 8 + created_unix: 946684810 + +- + id: 1002 + repo_id: 3 + type: 2 + created_unix: 946684810 + +- + id: 1003 + repo_id: 3 + type: 3 + created_unix: 946684810 + +- + id: 1004 + repo_id: 1001 + type: 8 + created_unix: 946684810 + +- + id: 1005 + repo_id: 1001 + type: 2 + created_unix: 946684810 + +- + id: 1006 + repo_id: 1001 + type: 3 + created_unix: 946684810 diff --git a/tests/integration/fixtures/TestAssignProject/repository.yml b/tests/integration/fixtures/TestAssignProject/repository.yml new file mode 100644 index 0000000000..3a2d6d637a --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/repository.yml @@ -0,0 +1,30 @@ +- + id: 1001 + owner_id: 1001 + owner_name: test_assign_project_org + lower_name: test_assign_project_repo + name: test_assign_project_repo + default_branch: master + num_watches: 4 + num_stars: 0 + num_forks: 0 + num_milestones: 3 + num_closed_milestones: 1 + num_projects: 1 + num_closed_projects: 0 + is_private: false + is_empty: false + is_archived: false + is_mirror: false + status: 0 + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 7597 + is_fsck_enabled: true + close_issues_via_commit_in_any_branch: false + created_unix: 1731254961 + updated_unix: 1731254961 + topics: '[]' + diff --git a/tests/integration/fixtures/TestAssignProject/team.yml b/tests/integration/fixtures/TestAssignProject/team.yml new file mode 100644 index 0000000000..6f295a3c65 --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/team.yml @@ -0,0 +1,32 @@ +- + id: 1001 + org_id: 3 + lower_name: writers + name: Writers + authorize: 2 # writer + num_repos: 3 + num_members: 1 + includes_all_repositories: false + can_create_org_repo: true + +- + id: 1002 + org_id: 1001 + lower_name: writers + name: Writers + authorize: 2 # writer + num_repos: 1 + num_members: 1 + includes_all_repositories: false + can_create_org_repo: true + +- + id: 1003 + org_id: 1001 + lower_name: owners + name: Owners + authorize: 4 + num_repos: 0 + num_members: 1 + includes_all_repositories: false + can_create_org_repo: true diff --git a/tests/integration/fixtures/TestAssignProject/team_repo.yml b/tests/integration/fixtures/TestAssignProject/team_repo.yml new file mode 100644 index 0000000000..9b34a817d9 --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/team_repo.yml @@ -0,0 +1,11 @@ +- + id: 1001 + org_id: 3 + team_id: 1001 + repo_id: 3 + +- + id: 1002 + org_id: 1001 + team_id: 1002 + repo_id: 1001 diff --git a/tests/integration/fixtures/TestAssignProject/team_unit.yml b/tests/integration/fixtures/TestAssignProject/team_unit.yml new file mode 100644 index 0000000000..d156983090 --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/team_unit.yml @@ -0,0 +1,35 @@ +- + id: 1001 + team_id: 1001 + type: 8 + access_mode: 2 + +- + id: 1002 + team_id: 1002 + type: 8 + access_mode: 1 + +- + id: 1003 + team_id: 1001 + type: 2 + access_mode: 2 + +- + id: 1004 + team_id: 1001 + type: 3 + access_mode: 2 + +- + id: 1005 + team_id: 1002 + type: 2 + access_mode: 2 + +- + id: 1006 + team_id: 1002 + type: 3 + access_mode: 2 diff --git a/tests/integration/fixtures/TestAssignProject/team_user.yml b/tests/integration/fixtures/TestAssignProject/team_user.yml new file mode 100644 index 0000000000..8061f1b9d8 --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/team_user.yml @@ -0,0 +1,11 @@ +- + id: 1001 + org_id: 3 + team_id: 1001 + uid: 5 + +- + id: 1002 + org_id: 23 + team_id: 1002 + uid: 5 diff --git a/tests/integration/fixtures/TestAssignProject/user.yml b/tests/integration/fixtures/TestAssignProject/user.yml new file mode 100644 index 0000000000..8ad7bd0e67 --- /dev/null +++ b/tests/integration/fixtures/TestAssignProject/user.yml @@ -0,0 +1,37 @@ +- + id: 1001 + lower_name: test_assign_project_org + name: test_assign_project_org + full_name: ' <<<< >> >> > >> > >>> >> ' + email: org1001@example.com + keep_email_private: false + email_notifications_preference: onmention + passwd: ZogKvWdyEx:password + passwd_hash_algo: dummy + must_change_password: false + login_source: 0 + login_name: test_assign_project_org + type: 1 + salt: ZogKvWdyEx + max_repo_creation: 1000 + is_active: false + is_admin: false + is_restricted: false + allow_git_hook: false + allow_import_local: false + allow_create_organization: true + prohibit_login: false + avatar: "" + avatar_email: org1001@example.com + use_custom_avatar: true + num_followers: 0 + num_following: 0 + num_stars: 0 + num_repos: 1 + num_teams: 1 + num_members: 1 + visibility: 0 + repo_admin_change_team_access: false + theme: "" + keep_activity_private: false + created_unix: 1672578020 diff --git a/tests/integration/issue_test.go b/tests/integration/issue_test.go index d764870bab..c0ecf6b3bc 100644 --- a/tests/integration/issue_test.go +++ b/tests/integration/issue_test.go @@ -19,6 +19,7 @@ import ( auth_model "forgejo.org/models/auth" "forgejo.org/models/db" issues_model "forgejo.org/models/issues" + org_model "forgejo.org/models/organization" project_model "forgejo.org/models/project" repo_model "forgejo.org/models/repo" unit_model "forgejo.org/models/unit" @@ -31,6 +32,7 @@ import ( api "forgejo.org/modules/structs" "forgejo.org/modules/test" "forgejo.org/modules/translation" + repo_service "forgejo.org/services/repository" files_service "forgejo.org/services/repository/files" user_service "forgejo.org/services/user" "forgejo.org/tests" @@ -1661,3 +1663,113 @@ func TestIssueUrlHandling(t *testing.T) { MakeRequest(t, req, http.StatusNotFound) }) } + +func TestIssueProjectSidebarMissing(t *testing.T) { + const ( + repoID = 4 + userID = 5 + ) + defer unittest.OverrideFixtures("tests/integration/fixtures/TestAssignProject/")() + defer tests.PrepareTestEnv(t)() + + ctx := t.Context() + + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID}) + session := loginUser(t, user.Name) + + issueURL := testNewIssue(t, session, user.Name, repo.Name, "Hello", "World") + t.Run("Sidebar showing - user project available", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".select-project.dropdown", true) + }) + + // Enable repository's project unit + projectUnit := repo_model.RepoUnit{ + RepoID: repo.ID, + Type: unit_model.TypeProjects, + } + require.NoError(t, repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, []repo_model.RepoUnit{projectUnit}, nil)) + + t.Run("Sidebar showing - repository project unit on", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".select-project.dropdown", true) + }) + + project_model.DeleteProjectByID(ctx, 1003) + // Disable repository's project unit + require.NoError(t, repo_service.UpdateRepositoryUnits(db.DefaultContext, repo, nil, []unit_model.Type{unit_model.TypeProjects})) + t.Run("Sidebar missing", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".select-project.dropdown", false) + }) + + // Team with project available + team := unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1001}) + require.NoError(t, team.LoadMembers(ctx)) + require.NoError(t, team.LoadRepositories(ctx)) + + user = team.Members[0] + repo = team.Repos[0] + org := team.GetOrg(ctx) + session = loginUser(t, user.Name) + + issueURL = testNewIssue(t, session, org.Name, repo.Name, "Hello", "World") + t.Run("Sidebar showing - org on & repo on", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".select-project.dropdown", true) + }) + + // Disable repository project unit + require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects})) + t.Run("Sidebar showing - org on & repo off", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".select-project.dropdown", true) + }) + + // Team with project disabled + team = unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1002}) + require.NoError(t, team.LoadMembers(ctx)) + require.NoError(t, team.LoadRepositories(ctx)) + + user = team.Members[0] + repo = team.Repos[0] + org = team.GetOrg(ctx) + session = loginUser(t, user.Name) + + require.NoError(t, project_model.DeleteProjectByID(db.DefaultContext, 1004)) + + issueURL = testNewIssue(t, session, org.Name, repo.Name, "Hello", "World") + t.Run("Sidebar showing - org off & repo on", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".select-project.dropdown", true) + }) + + // Disable repository project unit + require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects})) + t.Run("Sidebar missing - org off & repo off", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + req := NewRequest(t, "GET", issueURL) + resp := session.MakeRequest(t, req, http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + htmlDoc.AssertElement(t, ".select-project.dropdown", false) + }) +} diff --git a/tests/integration/project_test.go b/tests/integration/project_test.go index 629793602b..7583075b5f 100644 --- a/tests/integration/project_test.go +++ b/tests/integration/project_test.go @@ -7,12 +7,20 @@ package integration import ( "fmt" "net/http" + "path" + "strconv" + "strings" "testing" "forgejo.org/models/db" + issues_model "forgejo.org/models/issues" + org_model "forgejo.org/models/organization" project_model "forgejo.org/models/project" repo_model "forgejo.org/models/repo" + unit_model "forgejo.org/models/unit" "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + repo_service "forgejo.org/services/repository" "forgejo.org/tests" "github.com/stretchr/testify/assert" @@ -160,3 +168,117 @@ func TestChangeStatusProject(t *testing.T) { }) }) } + +func TestAssignProject(t *testing.T) { + defer unittest.OverrideFixtures("tests/integration/fixtures/TestAssignProject/")() + defer tests.PrepareTestEnv(t)() + + ctx := t.Context() + + newTestIssue := func(t *testing.T, session *TestSession, owner *user_model.User, repo *repo_model.Repository) (*issues_model.Issue, string, string) { + t.Helper() + + issueURL := testNewIssue(t, session, owner.Name, repo.Name, "Hello", "World") + indexStr := issueURL[strings.LastIndexByte(issueURL, '/')+1:] + index, err := strconv.Atoi(indexStr) + require.NoError(t, err, "Invalid issue href: %s", issueURL) + + issue := &issues_model.Issue{RepoID: repo.ID, Index: int64(index)} + unittest.AssertExistsAndLoadBean(t, issue) + + issueID := strconv.FormatInt(issue.ID, 10) + return issue, indexStr, issueID + } + + updateIssueProject := func(t *testing.T, session *TestSession, projectID, issueID, owner, repo string, expectedStatus int) { + t.Helper() + + req := NewRequestWithValues(t, "POST", path.Join(owner, repo, "issues", "projects"), map[string]string{ + "issue_ids": issueID, + "id": projectID, + }) + session.MakeRequest(t, req, expectedStatus) + } + + // User + t.Run("UserProjectOn+RepoProjectOff", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) + + session := loginUser(tt, user.Name) + issue, _, issueID := newTestIssue(tt, session, user, repo) + + updateIssueProject(tt, session, "1003", issueID, user.Name, repo.Name, http.StatusOK) + require.NoError(tt, issue.LoadProject(db.DefaultContext)) + require.NotNil(tt, issue.Project) + require.Equal(tt, int64(1003), issue.Project.ID) + }) + + // Team 1001 - enabled project unit + team := unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1001}) + require.NoError(t, team.LoadMembers(ctx)) + require.NoError(t, team.LoadRepositories(ctx)) + + user := team.Members[0] + repo := team.Repos[0] + org := team.GetOrg(ctx) + + session := loginUser(t, user.Name) + + t.Run("OrgProjectOn+RepoProjectOn", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo) + + updateIssueProject(tt, session, "1001", issueID, org.Name, repo.Name, http.StatusOK) + + require.NoError(tt, issue.LoadProject(db.DefaultContext)) + require.NotNil(tt, issue.Project) + require.Equal(tt, int64(1001), issue.Project.ID) + }) + + // Disable repository project unit + require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects})) + t.Run("OrgProjectOn+RepoProjectOff", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo) + + updateIssueProject(tt, session, "1001", issueID, org.Name, repo.Name, http.StatusOK) + require.NoError(tt, issue.LoadProject(db.DefaultContext)) + require.NotNil(tt, issue.Project) + require.Equal(tt, int64(1001), issue.Project.ID) + }) + + // Team 1002 - disabled project unit + team = unittest.AssertExistsAndLoadBean(t, &org_model.Team{ID: 1002}) + require.NoError(t, team.LoadMembers(ctx)) + require.NoError(t, team.LoadRepositories(ctx)) + + user = team.Members[0] + repo = team.Repos[0] + org = team.GetOrg(ctx) + + session = loginUser(t, user.Name) + + t.Run("OrgProjectOff+RepoProjectOn", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo) + + updateIssueProject(tt, session, "1002", issueID, org.Name, repo.Name, http.StatusOK) + require.NoError(tt, issue.LoadProject(db.DefaultContext)) + require.NotNil(tt, issue.Project) + require.Equal(tt, int64(1002), issue.Project.ID) + }) + + // Disable repository project unit + require.NoError(t, repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeProjects})) + t.Run("OrgProjectOff+RepoProjectOff", func(tt *testing.T) { + defer tests.PrintCurrentTest(tt)() + issue, _, issueID := newTestIssue(tt, session, org.AsUser(), repo) + + updateIssueProject(tt, session, "1002", issueID, org.Name, repo.Name, http.StatusOK) + require.NoError(tt, issue.LoadProject(db.DefaultContext)) + require.NotNil(tt, issue.Project) + require.Equal(tt, int64(1002), issue.Project.ID) + }) +}