mirror of
https://codeberg.org/davrot/forgejo.git
synced 2025-04-21 23:36:43 +02:00
Merge branch 'forgejo' into forgotten_password
This commit is contained in:
commit
edd06e2f5e
37 changed files with 682 additions and 55 deletions
|
@ -87,6 +87,9 @@ code.gitea.io/gitea/modules/eventsource
|
|||
Event.String
|
||||
|
||||
code.gitea.io/gitea/modules/forgefed
|
||||
NewForgeUndoLike
|
||||
ForgeUndoLike.UnmarshalJSON
|
||||
ForgeUndoLike.Validate
|
||||
GetItemByType
|
||||
JSONUnmarshalerFn
|
||||
NotEmpty
|
||||
|
@ -103,9 +106,6 @@ code.gitea.io/gitea/modules/git
|
|||
openRepositoryWithDefaultContext
|
||||
ToEntryMode
|
||||
|
||||
code.gitea.io/gitea/modules/gitgraph
|
||||
Parser.Reset
|
||||
|
||||
code.gitea.io/gitea/modules/gitrepo
|
||||
GetBranchCommitID
|
||||
GetWikiDefaultBranch
|
||||
|
@ -224,6 +224,9 @@ code.gitea.io/gitea/services/repository
|
|||
code.gitea.io/gitea/services/repository/files
|
||||
ContentType.String
|
||||
|
||||
code.gitea.io/gitea/services/repository/gitgraph
|
||||
Parser.Reset
|
||||
|
||||
code.gitea.io/gitea/services/webhook
|
||||
NewNotifier
|
||||
|
||||
|
|
2
go.mod
2
go.mod
|
@ -103,7 +103,7 @@ require (
|
|||
go.uber.org/mock v0.4.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/net v0.34.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
golang.org/x/sync v0.11.0
|
||||
golang.org/x/sys v0.30.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -1640,8 +1640,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
|||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
|
@ -21,8 +21,8 @@ type ForgeLike struct {
|
|||
func NewForgeLike(actorIRI, objectIRI string, startTime time.Time) (ForgeLike, error) {
|
||||
result := ForgeLike{}
|
||||
result.Type = ap.LikeType
|
||||
result.Actor = ap.IRI(actorIRI) // That's us, a User
|
||||
result.Object = ap.IRI(objectIRI) // That's them, a Repository
|
||||
result.Actor = ap.IRI(actorIRI)
|
||||
result.Object = ap.IRI(objectIRI)
|
||||
result.StartTime = startTime
|
||||
if valid, err := validation.IsValid(result); !valid {
|
||||
return ForgeLike{}, err
|
||||
|
@ -46,20 +46,23 @@ func (like ForgeLike) Validate() []string {
|
|||
var result []string
|
||||
result = append(result, validation.ValidateNotEmpty(string(like.Type), "type")...)
|
||||
result = append(result, validation.ValidateOneOf(string(like.Type), []any{"Like"}, "type")...)
|
||||
|
||||
if like.Actor == nil {
|
||||
result = append(result, "Actor should not be nil.")
|
||||
} else {
|
||||
result = append(result, validation.ValidateNotEmpty(like.Actor.GetID().String(), "actor")...)
|
||||
}
|
||||
if like.Object == nil {
|
||||
result = append(result, "Object should not be nil.")
|
||||
} else {
|
||||
result = append(result, validation.ValidateNotEmpty(like.Object.GetID().String(), "object")...)
|
||||
}
|
||||
|
||||
result = append(result, validation.ValidateNotEmpty(like.StartTime.String(), "startTime")...)
|
||||
if like.StartTime.IsZero() {
|
||||
result = append(result, "StartTime was invalid.")
|
||||
}
|
||||
|
||||
if like.Object == nil {
|
||||
result = append(result, "Object should not be nil.")
|
||||
} else {
|
||||
result = append(result, validation.ValidateNotEmpty(like.Object.GetID().String(), "object")...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
|
@ -16,11 +16,11 @@ import (
|
|||
)
|
||||
|
||||
func Test_NewForgeLike(t *testing.T) {
|
||||
want := []byte(`{"type":"Like","startTime":"2024-03-07T00:00:00Z","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`)
|
||||
|
||||
actorIRI := "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
|
||||
objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
|
||||
want := []byte(`{"type":"Like","startTime":"2024-03-27T00:00:00Z","actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1","object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}`)
|
||||
|
||||
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
|
||||
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-07")
|
||||
sut, err := NewForgeLike(actorIRI, objectIRI, startTime)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v\n", err)
|
||||
|
@ -84,7 +84,6 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
|
|||
wantErr error
|
||||
}
|
||||
|
||||
//revive:disable
|
||||
tests := map[string]testPair{
|
||||
"with ID": {
|
||||
item: []byte(`{"type":"Like","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"}`),
|
||||
|
@ -100,10 +99,9 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
|
|||
"invalid": {
|
||||
item: []byte(`{"type":"Invalid","actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1","object":"https://codeberg.org/api/activitypub/repository-id/1"`),
|
||||
want: &ForgeLike{},
|
||||
wantErr: fmt.Errorf("cannot parse JSON:"),
|
||||
wantErr: fmt.Errorf("cannot parse JSON"),
|
||||
},
|
||||
}
|
||||
//revive:enable
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
@ -120,7 +118,9 @@ func Test_LikeUnmarshalJSON(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestActivityValidation(t *testing.T) {
|
||||
func Test_ForgeLikeValidation(t *testing.T) {
|
||||
// Successful
|
||||
|
||||
sut := new(ForgeLike)
|
||||
sut.UnmarshalJSON([]byte(`{"type":"Like",
|
||||
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||
|
@ -130,35 +130,37 @@ func TestActivityValidation(t *testing.T) {
|
|||
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
|
||||
}
|
||||
|
||||
// Errors
|
||||
|
||||
sut.UnmarshalJSON([]byte(`{"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
||||
if sut.Validate()[0] != "type should not be empty" {
|
||||
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
|
||||
if err := validateAndCheckError(sut, "type should not be empty"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
sut.UnmarshalJSON([]byte(`{"type":"bad-type",
|
||||
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
||||
if sut.Validate()[0] != "Value bad-type is not contained in allowed values [Like]" {
|
||||
t.Errorf("validation error expected but was: %v\n", sut.Validate()[0])
|
||||
if err := validateAndCheckError(sut, "Value bad-type is not contained in allowed values [Like]"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
sut.UnmarshalJSON([]byte(`{"type":"Like",
|
||||
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||
"startTime": "not a date"}`))
|
||||
if sut.Validate()[0] != "StartTime was invalid." {
|
||||
t.Errorf("validation error expected but was: %v\n", sut.Validate())
|
||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||
"startTime": "not a date"}`))
|
||||
if err := validateAndCheckError(sut, "StartTime was invalid."); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
sut.UnmarshalJSON([]byte(`{"type":"Wrong",
|
||||
"actor":"https://repo.prod.meissa.de/api/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
||||
if sut.Validate()[0] != "Value Wrong is not contained in allowed values [Like]" {
|
||||
t.Errorf("validation error expected but was: %v\n", sut.Validate())
|
||||
"object":"https://codeberg.org/api/activitypub/repository-id/1",
|
||||
"startTime": "2014-12-31T23:00:00-08:00"}`))
|
||||
if err := validateAndCheckError(sut, "Value Wrong is not contained in allowed values [Like]"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,6 +168,6 @@ func TestActivityValidation_Attack(t *testing.T) {
|
|||
sut := new(ForgeLike)
|
||||
sut.UnmarshalJSON([]byte(`{rubbish}`))
|
||||
if len(sut.Validate()) != 5 {
|
||||
t.Errorf("5 validateion errors expected but was: %v\n", len(sut.Validate()))
|
||||
t.Errorf("5 validation errors expected but was: %v\n", len(sut.Validate()))
|
||||
}
|
||||
}
|
80
modules/forgefed/activity_undo_like.go
Normal file
80
modules/forgefed/activity_undo_like.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgefed
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
// ForgeLike activity data type
|
||||
// swagger:model
|
||||
type ForgeUndoLike struct {
|
||||
// swagger:ignore
|
||||
ap.Activity
|
||||
}
|
||||
|
||||
func NewForgeUndoLike(actorIRI, objectIRI string, startTime time.Time) (ForgeUndoLike, error) {
|
||||
result := ForgeUndoLike{}
|
||||
result.Type = ap.UndoType
|
||||
result.Actor = ap.IRI(actorIRI)
|
||||
result.StartTime = startTime
|
||||
|
||||
like := ap.Activity{}
|
||||
like.Type = ap.LikeType
|
||||
like.Actor = ap.IRI(actorIRI)
|
||||
like.Object = ap.IRI(objectIRI)
|
||||
result.Object = &like
|
||||
|
||||
if valid, err := validation.IsValid(result); !valid {
|
||||
return ForgeUndoLike{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (undo *ForgeUndoLike) UnmarshalJSON(data []byte) error {
|
||||
return undo.Activity.UnmarshalJSON(data)
|
||||
}
|
||||
|
||||
func (undo ForgeUndoLike) Validate() []string {
|
||||
var result []string
|
||||
result = append(result, validation.ValidateNotEmpty(string(undo.Type), "type")...)
|
||||
result = append(result, validation.ValidateOneOf(string(undo.Type), []any{"Undo"}, "type")...)
|
||||
|
||||
if undo.Actor == nil {
|
||||
result = append(result, "Actor should not be nil.")
|
||||
} else {
|
||||
result = append(result, validation.ValidateNotEmpty(undo.Actor.GetID().String(), "actor")...)
|
||||
}
|
||||
|
||||
result = append(result, validation.ValidateNotEmpty(undo.StartTime.String(), "startTime")...)
|
||||
if undo.StartTime.IsZero() {
|
||||
result = append(result, "StartTime was invalid.")
|
||||
}
|
||||
|
||||
if undo.Object == nil {
|
||||
result = append(result, "object should not be empty.")
|
||||
} else if activity, ok := undo.Object.(*ap.Activity); !ok {
|
||||
result = append(result, "object is not of type Activity")
|
||||
} else {
|
||||
result = append(result, validation.ValidateNotEmpty(string(activity.Type), "type")...)
|
||||
result = append(result, validation.ValidateOneOf(string(activity.Type), []any{"Like"}, "type")...)
|
||||
|
||||
if activity.Actor == nil {
|
||||
result = append(result, "Object.Actor should not be nil.")
|
||||
} else {
|
||||
result = append(result, validation.ValidateNotEmpty(activity.Actor.GetID().String(), "actor")...)
|
||||
}
|
||||
|
||||
if activity.Object == nil {
|
||||
result = append(result, "Object.Object should not be nil.")
|
||||
} else {
|
||||
result = append(result, validation.ValidateNotEmpty(activity.Object.GetID().String(), "object")...)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
246
modules/forgefed/activity_undo_like_test.go
Normal file
246
modules/forgefed/activity_undo_like_test.go
Normal file
|
@ -0,0 +1,246 @@
|
|||
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgefed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
)
|
||||
|
||||
func Test_NewForgeUndoLike(t *testing.T) {
|
||||
actorIRI := "https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
|
||||
objectIRI := "https://codeberg.org/api/v1/activitypub/repository-id/1"
|
||||
want := []byte(`{"type":"Undo","startTime":"2024-03-27T00:00:00Z",` +
|
||||
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
|
||||
`"object":{` +
|
||||
`"type":"Like",` +
|
||||
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
|
||||
`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`)
|
||||
|
||||
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
|
||||
sut, err := NewForgeUndoLike(actorIRI, objectIRI, startTime)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v\n", err)
|
||||
}
|
||||
if valid, _ := validation.IsValid(sut); !valid {
|
||||
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
|
||||
}
|
||||
|
||||
got, err := sut.MarshalJSON()
|
||||
if err != nil {
|
||||
t.Errorf("MarshalJSON() error = \"%v\"", err)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("MarshalJSON() got = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UndoLikeMarshalJSON(t *testing.T) {
|
||||
type testPair struct {
|
||||
item ForgeUndoLike
|
||||
want []byte
|
||||
wantErr error
|
||||
}
|
||||
|
||||
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
|
||||
like, _ := NewForgeLike("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1", "https://codeberg.org/api/v1/activitypub/repository-id/1", startTime)
|
||||
tests := map[string]testPair{
|
||||
"empty": {
|
||||
item: ForgeUndoLike{},
|
||||
want: nil,
|
||||
},
|
||||
"valid": {
|
||||
item: ForgeUndoLike{
|
||||
Activity: ap.Activity{
|
||||
StartTime: startTime,
|
||||
Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"),
|
||||
Type: "Undo",
|
||||
Object: like,
|
||||
},
|
||||
},
|
||||
want: []byte(`{"type":"Undo",` +
|
||||
`"startTime":"2024-03-27T00:00:00Z",` +
|
||||
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
|
||||
`"object":{` +
|
||||
`"type":"Like",` +
|
||||
`"startTime":"2024-03-27T00:00:00Z",` +
|
||||
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
|
||||
`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := tt.item.MarshalJSON()
|
||||
if (err != nil || tt.wantErr != nil) && tt.wantErr.Error() != err.Error() {
|
||||
t.Errorf("MarshalJSON() error = \"%v\", wantErr \"%v\"", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("MarshalJSON() got = %q\nwant %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UndoLikeUnmarshalJSON(t *testing.T) {
|
||||
type testPair struct {
|
||||
item []byte
|
||||
want *ForgeUndoLike
|
||||
wantErr error
|
||||
}
|
||||
|
||||
startTime, _ := time.Parse("2006-Jan-02", "2024-Mar-27")
|
||||
like, _ := NewForgeLike("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1", "https://codeberg.org/api/v1/activitypub/repository-id/1", startTime)
|
||||
|
||||
tests := map[string]testPair{
|
||||
"valid": {
|
||||
item: []byte(`{"type":"Undo",` +
|
||||
`"startTime":"2024-03-27T00:00:00Z",` +
|
||||
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
|
||||
`"object":{` +
|
||||
`"type":"Like",` +
|
||||
`"startTime":"2024-03-27T00:00:00Z",` +
|
||||
`"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",` +
|
||||
`"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`),
|
||||
want: &ForgeUndoLike{
|
||||
Activity: ap.Activity{
|
||||
StartTime: startTime,
|
||||
Actor: ap.IRI("https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"),
|
||||
Type: "Undo",
|
||||
Object: like,
|
||||
},
|
||||
},
|
||||
wantErr: nil,
|
||||
},
|
||||
"invalid": {
|
||||
item: []byte(`invalid JSON`),
|
||||
want: nil,
|
||||
wantErr: fmt.Errorf("cannot parse JSON"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := new(ForgeUndoLike)
|
||||
err := got.UnmarshalJSON(test.item)
|
||||
if test.wantErr != nil {
|
||||
if err == nil {
|
||||
t.Errorf("UnmarshalJSON() error = nil, wantErr \"%v\"", test.wantErr)
|
||||
} else if !strings.Contains(err.Error(), test.wantErr.Error()) {
|
||||
t.Errorf("UnmarshalJSON() error = \"%v\", wantErr \"%v\"", err, test.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
remarshalledgot, _ := got.MarshalJSON()
|
||||
remarshalledwant, _ := test.want.MarshalJSON()
|
||||
if !reflect.DeepEqual(remarshalledgot, remarshalledwant) {
|
||||
t.Errorf("UnmarshalJSON() got = %#v\nwant %#v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivityValidationUndo(t *testing.T) {
|
||||
sut := new(ForgeUndoLike)
|
||||
|
||||
_ = sut.UnmarshalJSON([]byte(`
|
||||
{"type":"Undo",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":{
|
||||
"type":"Like",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
|
||||
if res, _ := validation.IsValid(sut); !res {
|
||||
t.Errorf("sut expected to be valid: %v\n", sut.Validate())
|
||||
}
|
||||
|
||||
_ = sut.UnmarshalJSON([]byte(`
|
||||
{"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":{
|
||||
"type":"Like",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
|
||||
if err := validateAndCheckError(sut, "type should not be empty"); err != nil {
|
||||
t.Error(*err)
|
||||
}
|
||||
|
||||
_ = sut.UnmarshalJSON([]byte(`
|
||||
{"type":"Undo",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"object":{
|
||||
"type":"Like",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
|
||||
if err := validateAndCheckError(sut, "Actor should not be nil."); err != nil {
|
||||
t.Error(*err)
|
||||
}
|
||||
|
||||
_ = sut.UnmarshalJSON([]byte(`
|
||||
{"type":"Undo",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"string",
|
||||
"object":{
|
||||
"type":"Like",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
|
||||
if err := validateAndCheckError(sut, "Actor should not be nil."); err != nil {
|
||||
t.Error(*err)
|
||||
}
|
||||
|
||||
_ = sut.UnmarshalJSON([]byte(`
|
||||
{"type":"Undo",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"
|
||||
}`))
|
||||
if err := validateAndCheckError(sut, "object should not be empty."); err != nil {
|
||||
t.Error(*err)
|
||||
}
|
||||
|
||||
_ = sut.UnmarshalJSON([]byte(`
|
||||
{"type":"Undo",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":{
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":"https://codeberg.org/api/v1/activitypub/repository-id/1"}}`))
|
||||
if err := validateAndCheckError(sut, "object is not of type Activity"); err != nil {
|
||||
t.Error(*err)
|
||||
}
|
||||
|
||||
_ = sut.UnmarshalJSON([]byte(`
|
||||
{"type":"Undo",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":{
|
||||
"type":"Like",
|
||||
"object":""}}`))
|
||||
if err := validateAndCheckError(sut, "Object.Actor should not be nil."); err != nil {
|
||||
t.Error(*err)
|
||||
}
|
||||
|
||||
_ = sut.UnmarshalJSON([]byte(`
|
||||
{"type":"Undo",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1",
|
||||
"object":{
|
||||
"type":"Like",
|
||||
"startTime":"2024-03-27T00:00:00Z",
|
||||
"actor":"https://repo.prod.meissa.de/api/v1/activitypub/user-id/1"}}`))
|
||||
if err := validateAndCheckError(sut, "Object.Object should not be nil."); err != nil {
|
||||
t.Error(*err)
|
||||
}
|
||||
}
|
23
modules/forgefed/activity_validateandcheckerror_test.go
Normal file
23
modules/forgefed/activity_validateandcheckerror_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2023, 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgefed
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
)
|
||||
|
||||
func validateAndCheckError(subject validation.Validateable, expectedError string) *string {
|
||||
errors := subject.Validate()
|
||||
err := errors[0]
|
||||
if len(errors) < 1 {
|
||||
val := "Validation error should have been returned, but was not."
|
||||
return &val
|
||||
} else if err != expectedError {
|
||||
val := fmt.Sprintf("Validation error should be [%v] but was: %v\n", expectedError, err)
|
||||
return &val
|
||||
}
|
||||
return nil
|
||||
}
|
119
options/gitignore/Flutter
Normal file
119
options/gitignore/Flutter
Normal file
|
@ -0,0 +1,119 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.lock
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.buildlog/
|
||||
.history
|
||||
|
||||
|
||||
|
||||
# Flutter repo-specific
|
||||
/bin/cache/
|
||||
/bin/internal/bootstrap.bat
|
||||
/bin/internal/bootstrap.sh
|
||||
/bin/mingit/
|
||||
/dev/benchmarks/mega_gallery/
|
||||
/dev/bots/.recipe_deps
|
||||
/dev/bots/android_tools/
|
||||
/dev/devicelab/ABresults*.json
|
||||
/dev/docs/doc/
|
||||
/dev/docs/flutter.docs.zip
|
||||
/dev/docs/lib/
|
||||
/dev/docs/pubspec.yaml
|
||||
/dev/integration_tests/**/xcuserdata
|
||||
/dev/integration_tests/**/Pods
|
||||
/packages/flutter/coverage/
|
||||
version
|
||||
analysis_benchmark.json
|
||||
|
||||
# packages file containing multi-root paths
|
||||
.packages.generated
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
**/generated_plugin_registrant.dart
|
||||
.packages
|
||||
.pub-preload-cache/
|
||||
.pub/
|
||||
build/
|
||||
flutter_*.png
|
||||
linked_*.ds
|
||||
unlinked.ds
|
||||
unlinked_spec.ds
|
||||
|
||||
# Android related
|
||||
**/android/**/gradle-wrapper.jar
|
||||
.gradle/
|
||||
**/android/captures/
|
||||
**/android/gradlew
|
||||
**/android/gradlew.bat
|
||||
**/android/local.properties
|
||||
**/android/**/GeneratedPluginRegistrant.java
|
||||
**/android/key.properties
|
||||
*.jks
|
||||
|
||||
# iOS/XCode related
|
||||
**/ios/**/*.mode1v3
|
||||
**/ios/**/*.mode2v3
|
||||
**/ios/**/*.moved-aside
|
||||
**/ios/**/*.pbxuser
|
||||
**/ios/**/*.perspectivev3
|
||||
**/ios/**/*sync/
|
||||
**/ios/**/.sconsign.dblite
|
||||
**/ios/**/.tags*
|
||||
**/ios/**/.vagrant/
|
||||
**/ios/**/DerivedData/
|
||||
**/ios/**/Icon?
|
||||
**/ios/**/Pods/
|
||||
**/ios/**/.symlinks/
|
||||
**/ios/**/profile
|
||||
**/ios/**/xcuserdata
|
||||
**/ios/.generated/
|
||||
**/ios/Flutter/.last_build_id
|
||||
**/ios/Flutter/App.framework
|
||||
**/ios/Flutter/Flutter.framework
|
||||
**/ios/Flutter/Flutter.podspec
|
||||
**/ios/Flutter/Generated.xcconfig
|
||||
**/ios/Flutter/ephemeral
|
||||
**/ios/Flutter/app.flx
|
||||
**/ios/Flutter/app.zip
|
||||
**/ios/Flutter/flutter_assets/
|
||||
**/ios/Flutter/flutter_export_environment.sh
|
||||
**/ios/ServiceDefinitions.json
|
||||
**/ios/Runner/GeneratedPluginRegistrant.*
|
||||
|
||||
# macOS
|
||||
**/Flutter/ephemeral/
|
||||
**/Pods/
|
||||
**/macos/Flutter/GeneratedPluginRegistrant.swift
|
||||
**/macos/Flutter/ephemeral
|
||||
**/xcuserdata/
|
||||
|
||||
# Windows
|
||||
**/windows/flutter/generated_plugin_registrant.cc
|
||||
**/windows/flutter/generated_plugin_registrant.h
|
||||
**/windows/flutter/generated_plugins.cmake
|
||||
|
||||
# Linux
|
||||
**/linux/flutter/generated_plugin_registrant.cc
|
||||
**/linux/flutter/generated_plugin_registrant.h
|
||||
**/linux/flutter/generated_plugins.cmake
|
||||
|
||||
# Coverage
|
||||
coverage/
|
||||
|
||||
# Symbols
|
||||
app.*.symbols
|
||||
|
||||
# Exceptions to above rules.
|
||||
!**/ios/**/default.mode1v3
|
||||
!**/ios/**/default.mode2v3
|
||||
!**/ios/**/default.pbxuser
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
!/dev/ci/**/Gemfile.lock
|
|
@ -1,3 +1,6 @@
|
|||
# Ignore build outputs from performing a nix-build or `nix build` command
|
||||
result
|
||||
result-*
|
||||
|
||||
# Ignore automatically generated direnv output
|
||||
.direnv
|
||||
|
|
16
options/gitignore/NotesAndCoreConfiguration
Normal file
16
options/gitignore/NotesAndCoreConfiguration
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Excludes Obsidian workspace cache and plugins. All notes and core obsidian
|
||||
# configuration files are tracked by Git.
|
||||
|
||||
# The current application UI state (DOM layout, recently-opened files, etc.) is
|
||||
# stored in these files (separate for desktop and mobile) so you can resume
|
||||
# your session seamlessly after a restart. If you want to track UI state, use
|
||||
# the Workspaces core plugin instead of relying on these files.
|
||||
.obsidian/workspace.json
|
||||
.obsidian/workspace-mobile.json
|
||||
|
||||
# Obsidian plugins are stored under .obsidian/plugins/$plugin_name. They
|
||||
# contain metadata (manifest.json), application code (main.js), stylesheets
|
||||
# (styles.css), and user-configuration data (data.json).
|
||||
# We want to exclude all plugin-related files, so we can exclude everything
|
||||
# under this directory.
|
||||
.obsidian/plugins/**/*
|
38
options/gitignore/NotesAndExtendedConfiguration
Normal file
38
options/gitignore/NotesAndExtendedConfiguration
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Excludes Obsidian workspace cache and plugin code, but retains plugin
|
||||
# configuration. All notes and user-controlled configuration files are tracked
|
||||
# by Git.
|
||||
#
|
||||
# !!! WARNING !!!
|
||||
#
|
||||
# Community plugins may store sensitive secrets in their data.json files. By
|
||||
# including these files, those secrets may be tracked in your Git repository.
|
||||
#
|
||||
# To ignore configurations for specific plugins, add a line like this after the
|
||||
# contents of this file (order is important):
|
||||
# .obsidian/plugins/{{plugin_name}}/data.json
|
||||
#
|
||||
# Alternatively, ensure that you are treating your entire Git repository as
|
||||
# sensitive data, since it may contain secrets, or may have contained them in
|
||||
# past commits. Understand your threat profile, and make the decision
|
||||
# appropriate for yourself. If in doubt, err on the side of not including
|
||||
# plugin configuration. Use one of the alternative gitignore files instead:
|
||||
# * NotesOnly.gitignore
|
||||
# * NotesAndCoreConfiguration.gitignore
|
||||
|
||||
# The current application UI state (DOM layout, recently-opened files, etc.) is
|
||||
# stored in these files (separate for desktop and mobile) so you can resume
|
||||
# your session seamlessly after a restart. If you want to track UI state, use
|
||||
# the Workspaces core plugin instead of relying on these files.
|
||||
.obsidian/workspace.json
|
||||
.obsidian/workspace-mobile.json
|
||||
|
||||
# Obsidian plugins are stored under .obsidian/plugins/$plugin_name. They
|
||||
# contain metadata (manifest.json), application code (main.js), stylesheets
|
||||
# (styles.css), and user-configuration data (data.json).
|
||||
# We only want to track data.json, so we:
|
||||
# 1. exclude everything under the plugins directory recursively,
|
||||
# 2. unignore the plugin directories themselves, which then allows us to
|
||||
# 3. unignore the data.json files
|
||||
.obsidian/plugins/**/*
|
||||
!.obsidian/plugins/*/
|
||||
!.obsidian/plugins/*/data.json
|
4
options/gitignore/NotesOnly
Normal file
4
options/gitignore/NotesOnly
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Excludes all Obsidian-related configuration. All notes are tracked by Git.
|
||||
|
||||
# All Obsidian configuration and runtime state is stored here
|
||||
.obsidian/**/*
|
|
@ -1996,6 +1996,9 @@ pulls.reopen_failed.base_branch = The pull request cannot be reopened, because t
|
|||
pulls.made_using_agit = AGit
|
||||
pulls.agit_explanation = Created using the AGit workflow. AGit lets contributors propose changes using "git push" without creating a fork or a new branch.
|
||||
|
||||
pulls.editable = Editable
|
||||
pulls.editable_explanation = This pull request allows edits from maintainers. You can contribute directly to it.
|
||||
|
||||
pulls.auto_merge_button_when_succeed = (When checks succeed)
|
||||
pulls.auto_merge_when_succeed = Auto merge when all checks succeed
|
||||
pulls.auto_merge_newly_scheduled = The pull request was scheduled to merge when all checks succeed.
|
||||
|
@ -2888,8 +2891,8 @@ settings.update_settings = Update settings
|
|||
settings.update_setting_success = Organization settings have been updated.
|
||||
settings.change_orgname_prompt = Note: Changing the organization name will also change your organization's URL and free the old name.
|
||||
settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed.
|
||||
settings.change_orgname_redirect_prompt.with_cooldown.one = The old username will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old username during the cooldown period.
|
||||
settings.change_orgname_redirect_prompt.with_cooldown.few = The old username will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old username during the cooldown period.
|
||||
settings.change_orgname_redirect_prompt.with_cooldown.one = The old organization name will be available to everyone after a cooldown period of %[1]d day, you can still reclaim the old name during the cooldown period.
|
||||
settings.change_orgname_redirect_prompt.with_cooldown.few = The old organization name will be available to everyone after a cooldown period of %[1]d days, you can still reclaim the old name during the cooldown period.
|
||||
settings.update_avatar_success = The organization's avatar has been updated.
|
||||
settings.delete = Delete organization
|
||||
settings.delete_account = Delete this organization
|
||||
|
|
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -37,7 +37,7 @@
|
|||
"monaco-editor": "0.52.2",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"pdfobject": "2.3.0",
|
||||
"postcss": "8.5.1",
|
||||
"postcss": "8.5.2",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-nesting": "13.0.1",
|
||||
"pretty-ms": "9.0.0",
|
||||
|
@ -12643,9 +12643,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
|
||||
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
|
||||
"integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"monaco-editor": "0.52.2",
|
||||
"monaco-editor-webpack-plugin": "7.1.0",
|
||||
"pdfobject": "2.3.0",
|
||||
"postcss": "8.5.1",
|
||||
"postcss": "8.5.2",
|
||||
"postcss-loader": "8.1.1",
|
||||
"postcss-nesting": "13.0.1",
|
||||
"pretty-ms": "9.0.0",
|
||||
|
|
|
@ -70,8 +70,8 @@ func RepositoryInbox(ctx *context.APIContext) {
|
|||
|
||||
repository := ctx.Repo.Repository
|
||||
log.Info("RepositoryInbox: repo: %v", repository)
|
||||
|
||||
form := web.GetForm(ctx)
|
||||
// TODO: Decide between like/undo{like} activity
|
||||
httpStatus, title, err := federation.ProcessLikeActivity(ctx, form, repository.ID)
|
||||
if err != nil {
|
||||
ctx.Error(httpStatus, title, err)
|
||||
|
|
|
@ -95,10 +95,9 @@ func stripSlashesMiddleware(next http.Handler) http.Handler {
|
|||
prevWasSlash = chr == '/'
|
||||
}
|
||||
|
||||
if rctx == nil {
|
||||
req.URL.Path = sanitizedPath.String()
|
||||
} else {
|
||||
rctx.RoutePath = sanitizedPath.String()
|
||||
req.URL.Path = sanitizedPath.String()
|
||||
if rctx != nil {
|
||||
rctx.RoutePath = req.URL.Path
|
||||
}
|
||||
next.ServeHTTP(resp, req)
|
||||
})
|
||||
|
|
|
@ -7,6 +7,9 @@ import (
|
|||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
|
||||
chi "github.com/go-chi/chi/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -43,6 +46,11 @@ func TestStripSlashesMiddleware(t *testing.T) {
|
|||
inputPath: "/user2//repo1/",
|
||||
expectedPath: "/user2/repo1",
|
||||
},
|
||||
{
|
||||
name: "path with slashes in the beginning",
|
||||
inputPath: "https://codeberg.org//user2/repo1/",
|
||||
expectedPath: "/user2/repo1",
|
||||
},
|
||||
{
|
||||
name: "path with slashes and query params",
|
||||
inputPath: "/repo//migrate?service_type=3",
|
||||
|
@ -56,15 +64,22 @@ func TestStripSlashesMiddleware(t *testing.T) {
|
|||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
testMiddleware := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
r := web.NewRoute()
|
||||
r.Use(stripSlashesMiddleware)
|
||||
|
||||
called := false
|
||||
r.Get("*", func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, tt.expectedPath, r.URL.Path)
|
||||
|
||||
rctx := chi.RouteContext(r.Context())
|
||||
assert.Equal(t, tt.expectedPath, rctx.RoutePath)
|
||||
|
||||
called = true
|
||||
})
|
||||
|
||||
// pass the test middleware to validate the changes
|
||||
handlerToTest := stripSlashesMiddleware(testMiddleware)
|
||||
// create a mock request to use
|
||||
req := httptest.NewRequest("GET", tt.inputPath, nil)
|
||||
// call the handler using a mock response recorder
|
||||
handlerToTest.ServeHTTP(httptest.NewRecorder(), req)
|
||||
r.ServeHTTP(httptest.NewRecorder(), req)
|
||||
assert.True(t, called)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import (
|
|||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitgraph"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
|
@ -32,6 +31,7 @@ import (
|
|||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/gitdiff"
|
||||
git_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/services/repository/gitgraph"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -1058,7 +1058,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
|||
|
||||
if refType == RepoRefLegacy {
|
||||
// redirect from old URL scheme to new URL scheme
|
||||
prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.Params("*"))), strings.ToLower(ctx.Repo.RepoLink))
|
||||
prefix := strings.TrimPrefix(setting.AppSubURL+strings.ToLower(strings.TrimSuffix(ctx.Req.URL.Path, ctx.PathParamRaw("*"))), strings.ToLower(ctx.Repo.RepoLink))
|
||||
|
||||
ctx.Redirect(path.Join(
|
||||
ctx.Repo.RepoLink,
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
|
||||
<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="tw-text-center" colspan="7">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -66,6 +66,8 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="tw-text-center" colspan="6">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
<td nowrap>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
|
||||
<td class="view-detail"><a href="#">{{svg "octicon-note" 16}}</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="tw-text-center" colspan="6">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
{{if .Notices}}
|
||||
|
|
|
@ -66,6 +66,8 @@
|
|||
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
|
||||
<td><a href="{{.OrganisationLink}}/settings" data-tooltip-content="{{ctx.Locale.Tr "edit"}}">{{svg "octicon-pencil"}}</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="tw-text-center" colspan="7">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -74,6 +74,8 @@
|
|||
<td>{{DateUtils.AbsoluteShort .Version.CreatedUnix}}</td>
|
||||
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.Version.ID}}" data-name="{{.Package.Name}}" data-data-version="{{.Version.Version}}">{{svg "octicon-trash"}}</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="tw-text-center" colspan="10">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -86,6 +86,8 @@
|
|||
<td>{{DateUtils.AbsoluteShort .CreatedUnix}}</td>
|
||||
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.ID}}" data-name="{{.Name}}">{{svg "octicon-trash"}}</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td class="tw-text-center" colspan="12">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -109,6 +109,8 @@
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr class="no-results-row"><td class="tw-text-center" colspan="9">{{ctx.Locale.Tr "no_results_found"}}</td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -95,6 +95,11 @@
|
|||
</span>
|
||||
</a>
|
||||
{{end}}
|
||||
{{if and .Issue.PullRequest.AllowMaintainerEdit .CanWriteCode}}
|
||||
<span id="editable-label" data-tooltip-content="{{ctx.Locale.Tr "repo.pulls.editable_explanation"}}" class="ui small label">
|
||||
{{ctx.Locale.Tr "repo.pulls.editable"}}
|
||||
</span>
|
||||
{{end}}
|
||||
<span id="pull-desc-editor" class="tw-hidden flex-text-block" data-target-update-url="{{$.RepoLink}}/pull/{{.Issue.Index}}/target_branch">
|
||||
<div class="ui floating filter dropdown">
|
||||
<div class="ui basic small button tw-mr-0">
|
||||
|
|
|
@ -1 +1 @@
|
|||
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1">bot</span>{{end}}
|
||||
<a class="author text black tw-font-semibold muted"{{if gt .ID 0}} href="{{.HomeLink}}"{{end}}>{{.GetDisplayName}}</a>{{if .IsBot}}<span class="ui basic label tw-p-1 tw-align-baseline">bot</span>{{end}}
|
||||
|
|
|
@ -283,7 +283,7 @@ func TestLDAPUserSyncWithEmptyUsernameAttribute(t *testing.T) {
|
|||
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
tr := htmlDoc.doc.Find("table.table tbody tr")
|
||||
tr := htmlDoc.doc.Find("table.table tbody tr:not(.no-results-row)")
|
||||
assert.Equal(t, 0, tr.Length())
|
||||
}
|
||||
|
||||
|
|
54
tests/integration/pull_editable_test.go
Normal file
54
tests/integration/pull_editable_test.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/tests"
|
||||
)
|
||||
|
||||
func TestPullEditable_ShowEditableLabel(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, forgejoURL *url.URL) {
|
||||
t.Run("Show editable label if PR is editable", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
editable := true
|
||||
|
||||
setPREditable(t, editable)
|
||||
testEditableLabelShown(t, editable)
|
||||
})
|
||||
|
||||
t.Run("Don't show editable label if PR is not editable", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
editable := false
|
||||
|
||||
setPREditable(t, editable)
|
||||
testEditableLabelShown(t, editable)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func setPREditable(t *testing.T, editable bool) {
|
||||
t.Helper()
|
||||
session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/repos/user2/repo1/pulls/3", &api.EditPullRequestOption{
|
||||
AllowMaintainerEdit: &editable,
|
||||
}).AddTokenAuth(token)
|
||||
session.MakeRequest(t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
func testEditableLabelShown(t *testing.T, expectLabel bool) {
|
||||
t.Helper()
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequest(t, "GET", "/user2/repo1/pulls/3")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
htmlDoc.AssertElement(t, "#editable-label", expectLabel)
|
||||
}
|
|
@ -167,7 +167,7 @@ func TestUserRedirect(t *testing.T) {
|
|||
defer test.MockVariableValue(&setting.Service.UsernameCooldownPeriod, 8)()
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
assert.Contains(t, getPrompt(t), "The old username will be available to everyone after a cooldown period of 8 days, you can still reclaim the old username during the cooldown period.")
|
||||
assert.Contains(t, getPrompt(t), "The old organization name will be available to everyone after a cooldown period of 8 days, you can still reclaim the old name during the cooldown period.")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue