Merge branch 'main' into main

Signed-off-by: Karthik Bhandary <34509856+karthikbhandary2@users.noreply.github.com>
This commit is contained in:
Karthik Bhandary authored and GitHub committed 2026-03-27 22:00:27 +05:30
commit 1633be5ddd
125 files changed
+3959 -3309

No files matched your search

+256 -94
View File
@@ -11,23 +11,28 @@ import (
"encoding/xml"
"fmt"
"io"
"mime"
"net/http"
"strings"
"testing"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/actions"
actions_service "code.gitea.io/gitea/services/actions"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -48,15 +53,18 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
assert.NoError(t, err)
table := []struct {
name string
version int32
blockID bool
noLength bool
append int
name string
version int32
contentType string
blockID bool
noLength bool
append int
path string
}{
{
name: "artifact",
version: 4,
path: "artifact.zip",
},
{
name: "artifact2",
@@ -98,6 +106,23 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
append: 4,
blockID: true,
},
{
name: "artifact9.json",
version: 7,
contentType: "application/json",
},
{
name: "artifact10",
version: 7,
contentType: "application/zip",
path: "artifact10.zip",
},
{
name: "artifact11.zip",
version: 7,
contentType: "application/zip",
path: "artifact11.zip",
},
}
for _, entry := range table {
@@ -108,6 +133,7 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
Name: entry.name,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil),
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
@@ -120,9 +146,8 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
blocks := make([]string, 0, util.Iif(entry.blockID, entry.append+1, 0))
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
for i := range entry.append + 1 {
url := uploadResp.SignedUploadUrl[idx:]
url := uploadResp.SignedUploadUrl
// See https://learn.microsoft.com/en-us/rest/api/storageservices/append-block
// See https://learn.microsoft.com/en-us/rest/api/storageservices/put-block
if entry.blockID {
@@ -146,7 +171,7 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
if entry.blockID && entry.append > 0 {
// https://learn.microsoft.com/en-us/rest/api/storageservices/put-block-list
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// upload artifact blockList
blockList := &actions.BlockList{
Latest: blocks,
@@ -174,6 +199,19 @@ func TestActionsArtifactV4UploadSingleFile(t *testing.T) {
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId})
if entry.contentType != "" {
assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType)
} else {
assert.Equal(t, "application/zip", artifact.ContentEncodingOrType)
}
if entry.path != "" {
assert.Equal(t, entry.path, artifact.ArtifactPath)
}
assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status)
assert.Equal(t, int64(entry.append+1)*1024, artifact.FileSize)
assert.Equal(t, int64(entry.append+1)*1024, artifact.FileCompressedSize)
})
}
}
@@ -198,8 +236,7 @@ func TestActionsArtifactV4UploadSingleFileWrongChecksum(t *testing.T) {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
url := uploadResp.SignedUploadUrl + "&comp=block"
// upload artifact chunk
body := strings.Repeat("B", 1024)
@@ -243,8 +280,7 @@ func TestActionsArtifactV4UploadSingleFileWithRetentionDays(t *testing.T) {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload url
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block"
url := uploadResp.SignedUploadUrl + "&comp=block"
// upload artifact chunk
body := strings.Repeat("A", 1024)
@@ -290,9 +326,8 @@ func TestActionsArtifactV4UploadSingleFileWithPotentialHarmfulBlockID(t *testing
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload urls
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
url := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=%2f..%2fmyfile"
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
url := uploadResp.SignedUploadUrl + "&comp=block&blockid=%2f..%2fmyfile"
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// upload artifact chunk
body := strings.Repeat("A", 1024)
@@ -339,63 +374,126 @@ func TestActionsArtifactV4UploadSingleFileWithChunksOutOfOrder(t *testing.T) {
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
assert.NoError(t, err)
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: 4,
Name: "artifactWithChunksOutOfOrder",
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
// get upload urls
idx := strings.Index(uploadResp.SignedUploadUrl, "/twirp/")
block1URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block1"
block2URL := uploadResp.SignedUploadUrl[idx:] + "&comp=block&blockid=block2"
blockListURL := uploadResp.SignedUploadUrl[idx:] + "&comp=blocklist"
// upload artifact chunks
bodyb := strings.Repeat("B", 1024)
req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb))
MakeRequest(t, req, http.StatusCreated)
bodya := strings.Repeat("A", 1024)
req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya))
MakeRequest(t, req, http.StatusCreated)
// upload artifact blockList
blockList := &actions.BlockList{
Latest: []string{
"block1",
"block2",
},
table := []struct {
name string
artifactName string
serveDirect bool
contentType string
}{
{name: "Upload-Zip", artifactName: "artifact-v4-upload", contentType: ""},
{name: "Upload-Pdf", artifactName: "report-upload.pdf", contentType: "application/pdf"},
{name: "Upload-Html", artifactName: "report-upload.html", contentType: "application/html"},
{name: "ServeDirect-Zip", artifactName: "artifact-v4-upload-serve-direct", contentType: "", serveDirect: true},
{name: "ServeDirect-Pdf", artifactName: "report-upload-serve-direct.pdf", contentType: "application/pdf", serveDirect: true},
{name: "ServeDirect-Html", artifactName: "report-upload-serve-direct.html", contentType: "application/html", serveDirect: true},
}
rawBlockList, err := xml.Marshal(blockList)
assert.NoError(t, err)
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
MakeRequest(t, req, http.StatusCreated)
t.Logf("Create artifact confirm")
for _, entry := range table {
t.Run(entry.name, func(t *testing.T) {
// Only AzureBlobStorageType supports ServeDirect Uploads
switch setting.Actions.ArtifactStorage.Type {
case setting.AzureBlobStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.serveDirect)()
default:
if entry.serveDirect {
t.Skip()
}
}
// acquire artifact upload url
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact", toProtoJSON(&actions.CreateArtifactRequest{
Version: util.Iif[int32](entry.contentType != "", 7, 4),
Name: entry.artifactName,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
MimeType: util.Iif(entry.contentType != "", wrapperspb.String(entry.contentType), nil),
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var uploadResp actions.CreateArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &uploadResp)
assert.True(t, uploadResp.Ok)
if !entry.serveDirect {
assert.Contains(t, uploadResp.SignedUploadUrl, "/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact")
}
sha := sha256.Sum256([]byte(bodya + bodyb))
// get upload urls
block1URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block1"))
block2URL := uploadResp.SignedUploadUrl + "&comp=block&blockid=" + base64.RawURLEncoding.EncodeToString([]byte("block2"))
blockListURL := uploadResp.SignedUploadUrl + "&comp=blocklist"
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: "artifactWithChunksOutOfOrder",
Size: 2048,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
// upload artifact chunks
bodyb := strings.Repeat("B", 1024)
req = NewRequestWithBody(t, "PUT", block2URL, strings.NewReader(bodyb))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
bodya := strings.Repeat("A", 1024)
req = NewRequestWithBody(t, "PUT", block1URL, strings.NewReader(bodya))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
// upload artifact blockList
blockList := &actions.BlockList{
Latest: []string{
base64.RawURLEncoding.EncodeToString([]byte("block1")),
base64.RawURLEncoding.EncodeToString([]byte("block2")),
},
}
rawBlockList, err := xml.Marshal(blockList)
assert.NoError(t, err)
req = NewRequestWithBody(t, "PUT", blockListURL, bytes.NewReader(rawBlockList))
if entry.serveDirect {
req.Request.RequestURI = ""
nresp, err := http.DefaultClient.Do(req.Request)
require.NoError(t, err)
nresp.Body.Close()
require.Equal(t, http.StatusCreated, nresp.StatusCode)
} else {
MakeRequest(t, req, http.StatusCreated)
}
t.Logf("Create artifact confirm")
sha := sha256.Sum256([]byte(bodya + bodyb))
// confirm artifact upload
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact", toProtoJSON(&actions.FinalizeArtifactRequest{
Name: entry.artifactName,
Size: 2048,
Hash: wrapperspb.String("sha256:" + hex.EncodeToString(sha[:])),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.FinalizeArtifactResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.True(t, finalizeResp.Ok)
artifact := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionArtifact{ID: finalizeResp.ArtifactId})
if entry.contentType != "" {
assert.Equal(t, entry.contentType, artifact.ContentEncodingOrType)
} else {
assert.Equal(t, "application/zip", artifact.ContentEncodingOrType)
}
assert.Equal(t, actions_model.ArtifactStatusUploadConfirmed, artifact.Status)
assert.Equal(t, int64(2048), artifact.FileSize)
assert.Equal(t, int64(2048), artifact.FileCompressedSize)
})
}
}
func TestActionsArtifactV4DownloadSingle(t *testing.T) {
@@ -404,33 +502,97 @@ func TestActionsArtifactV4DownloadSingle(t *testing.T) {
token, err := actions_service.CreateAuthorizationToken(48, 792, 193)
assert.NoError(t, err)
// list artifacts by name
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
NameFilter: wrapperspb.String("artifact-v4-download"),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var listResp actions.ListArtifactsResponse
protojson.Unmarshal(resp.Body.Bytes(), &listResp)
assert.Len(t, listResp.Artifacts, 1)
table := []struct {
Name string
ArtifactName string
FileName string
ServeDirect bool
ContentType string
ContentDisposition string
}{
{Name: "Download-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip"},
{Name: "Download-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf"},
{Name: "Download-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html"},
{Name: "ServeDirect-Zip", ArtifactName: "artifact-v4-download", FileName: "artifact-v4-download.zip", ContentType: "application/zip", ServeDirect: true},
{Name: "ServeDirect-Pdf", ArtifactName: "report.pdf", FileName: "report.pdf", ContentType: "application/pdf", ServeDirect: true},
{Name: "ServeDirect-Html", ArtifactName: "report.html", FileName: "report.html", ContentType: "application/html", ServeDirect: true},
}
// acquire artifact download url
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
Name: "artifact-v4-download",
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.GetSignedArtifactURLResponse
protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp)
assert.NotEmpty(t, finalizeResp.SignedUrl)
for _, entry := range table {
t.Run(entry.Name, func(t *testing.T) {
switch setting.Actions.ArtifactStorage.Type {
case setting.AzureBlobStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.AzureBlobConfig.ServeDirect, entry.ServeDirect)()
case setting.MinioStorageType:
defer test.MockVariableValue(&setting.Actions.ArtifactStorage.MinioConfig.ServeDirect, entry.ServeDirect)()
default:
if entry.ServeDirect {
t.Skip()
}
}
req = NewRequest(t, "GET", finalizeResp.SignedUrl)
resp = MakeRequest(t, req, http.StatusOK)
body := strings.Repeat("D", 1024)
assert.Equal(t, body, resp.Body.String())
// list artifacts by name
req := NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
NameFilter: wrapperspb.String(entry.ArtifactName),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
var listResp actions.ListArtifactsResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
require.Len(t, listResp.Artifacts, 1)
// list artifacts by id
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts", toProtoJSON(&actions.ListArtifactsRequest{
IdFilter: wrapperspb.Int64(listResp.Artifacts[0].DatabaseId),
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &listResp))
assert.Len(t, listResp.Artifacts, 1)
// acquire artifact download url
req = NewRequestWithBody(t, "POST", "/twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL", toProtoJSON(&actions.GetSignedArtifactURLRequest{
Name: entry.ArtifactName,
WorkflowRunBackendId: "792",
WorkflowJobRunBackendId: "193",
})).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var finalizeResp actions.GetSignedArtifactURLResponse
require.NoError(t, protojson.Unmarshal(resp.Body.Bytes(), &finalizeResp))
assert.NotEmpty(t, finalizeResp.SignedUrl)
body := strings.Repeat("D", 1024)
var contentDisposition string
if entry.ServeDirect {
externalReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, finalizeResp.SignedUrl, nil)
require.NoError(t, err)
externalResp, err := http.DefaultClient.Do(externalReq)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, externalResp.StatusCode)
assert.Equal(t, entry.ContentType, externalResp.Header.Get("Content-Type"))
contentDisposition = externalResp.Header.Get("Content-Disposition")
buf := make([]byte, 1024)
n, err := io.ReadAtLeast(externalResp.Body, buf, len(buf))
externalResp.Body.Close()
require.NoError(t, err)
assert.Equal(t, len(buf), n)
assert.Equal(t, body, string(buf))
} else {
req = NewRequest(t, "GET", finalizeResp.SignedUrl)
resp = MakeRequest(t, req, http.StatusOK)
assert.Equal(t, entry.ContentType, resp.Header().Get("Content-Type"))
contentDisposition = resp.Header().Get("Content-Disposition")
assert.Equal(t, body, resp.Body.String())
}
disposition, param, err := mime.ParseMediaType(contentDisposition)
require.NoError(t, err)
assert.Equal(t, "inline", disposition)
assert.Equal(t, entry.FileName, param["filename"])
})
}
}
func TestActionsArtifactV4RunDownloadSinglePublicApi(t *testing.T) {
@@ -561,7 +723,7 @@ func TestActionsArtifactV4ListAndGetPublicApi(t *testing.T) {
for _, artifact := range listResp.Entries {
assert.Contains(t, artifact.URL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d", repo.FullName(), artifact.ID))
assert.Contains(t, artifact.ArchiveDownloadURL, fmt.Sprintf("/api/v1/repos/%s/actions/artifacts/%d/zip", repo.FullName(), artifact.ID))
req = NewRequestWithBody(t, "GET", listResp.Entries[0].URL, nil).
req = NewRequestWithBody(t, "GET", artifact.URL, nil).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
+33 -22
View File
@@ -141,34 +141,45 @@ func TestAPIOrgGeneral(t *testing.T) {
})
t.Run("OrgEdit", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "private",
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility)
var apiOrg api.Organization
DecodeJSON(t, resp, &apiOrg)
org3Edit := api.EditOrgOption{
FullName: new("new full name"),
Description: new("new description"),
Website: new("https://org3-new-website.example.com"),
Location: new("new location"),
Visibility: new("limited"),
Email: new("org3-new-email@example.com"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token)
resp := MakeRequest(t, req, http.StatusOK)
apiOrg := DecodeJSON(t, resp, &api.Organization{})
assert.Equal(t, "org3", apiOrg.Name)
assert.Equal(t, org.FullName, apiOrg.FullName)
assert.Equal(t, org.Description, apiOrg.Description)
assert.Equal(t, org.Website, apiOrg.Website)
assert.Equal(t, org.Location, apiOrg.Location)
assert.Equal(t, org.Visibility, apiOrg.Visibility)
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
assert.Equal(t, *org3Edit.Description, apiOrg.Description)
assert.Equal(t, *org3Edit.Website, apiOrg.Website)
assert.Equal(t, *org3Edit.Location, apiOrg.Location)
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
assert.Equal(t, *org3Edit.Email, apiOrg.Email)
org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
assert.Equal(t, api.VisibleTypeLimited, org3.Visibility)
// empty email can clear the email, nil fields won't change the settings
req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{
Email: new(""),
}).AddTokenAuth(user1Token)
resp = MakeRequest(t, req, http.StatusOK)
apiOrg = DecodeJSON(t, resp, &api.Organization{})
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
assert.Empty(t, apiOrg.Email)
})
t.Run("OrgEditBadVisibility", func(t *testing.T) {
t.Run("OrgEditInvalidVisibility", func(t *testing.T) {
org := api.EditOrgOption{
FullName: "Org3 organization new full name",
Description: "A new description",
Website: "https://try.gitea.io/new",
Location: "Beijing",
Visibility: "badvisibility",
Visibility: new("invalid-visibility"),
}
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
+3 -3
View File
@@ -10,7 +10,6 @@ import (
"code.gitea.io/gitea/cmd"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
@@ -38,13 +37,14 @@ func Test_CmdKeys(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// FIXME: this test is not quite right. Each "command run" always re-initializes settings
defer test.MockVariableValue(&cmd.CmdKeys.Before, nil)() // don't re-initialize logger during the test
keysCmd := cmd.NewKeysCommand()
keysCmd.Before = nil // don't re-initialize logger during the test
var stdout, stderr bytes.Buffer
app := &cli.Command{
Writer: &stdout,
ErrWriter: &stderr,
Commands: []*cli.Command{cmd.CmdKeys},
Commands: []*cli.Command{keysCmd},
}
err := app.Run(t.Context(), append([]string{"prog"}, tt.args...))
if tt.wantErr {
+52 -72
View File
@@ -8,86 +8,66 @@ import (
"testing"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestDownloadByID(t *testing.T) {
func TestDownloadRepoContent(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Request raw blob
req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
resp := session.MakeRequest(t, req, http.StatusOK)
t.Run("RawBlob", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/raw/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
})
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
}
func TestDownloadByIDForSVGUsesSecureHeaders(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Request raw blob
req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
}
func TestDownloadByIDMedia(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Request raw blob
req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
}
func TestDownloadByIDMediaForSVGUsesSecureHeaders(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
// Request raw blob
req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
}
func TestDownloadRawTextFileWithoutMimeTypeMapping(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
}
func TestDownloadRawTextFileWithMimeTypeMapping(t *testing.T) {
defer tests.PrepareTestEnv(t)()
setting.MimeTypeMap.Map[".xml"] = "text/xml"
setting.MimeTypeMap.Enabled = true
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "text/xml; charset=utf-8", resp.Header().Get("Content-Type"))
delete(setting.MimeTypeMap.Map, ".xml")
setting.MimeTypeMap.Enabled = false
t.Run("SVGUsesSecureHeaders", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo2/raw/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
})
t.Run("MediaBlob", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo1/media/blob/4b4851ad51df6a7d9f25c979345979eaeb5b349f")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "# repo1\n\nDescription for repo1", resp.Body.String())
})
t.Run("MediaSVGUsesSecureHeaders", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo2/media/blob/6395b68e1feebb1e4c657b4f9f6ba2676a283c0b")
resp := session.MakeRequest(t, req, http.StatusOK)
assert.Equal(t, "default-src 'none'; style-src 'unsafe-inline'; sandbox", resp.Header().Get("Content-Security-Policy"))
assert.Equal(t, "image/svg+xml", resp.Header().Get("Content-Type"))
assert.Equal(t, "nosniff", resp.Header().Get("X-Content-Type-Options"))
})
t.Run("MimeTypeMap", func(t *testing.T) {
req := NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp := session.MakeRequest(t, req, http.StatusOK)
// although the file is a valid XML file, it is served as "text/plain" to avoid site content spamming (the same to "text/html" files)
assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type"))
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.MimeTypeMap)()
setting.MimeTypeMap.Enabled = true
setting.MimeTypeMap.Map[".xml"] = "text/xml"
req = NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp = session.MakeRequest(t, req, http.StatusOK)
// respect the mime mapping, and "text/plain" protection isn't used anymore
assert.Equal(t, "text/xml; charset=utf-8", resp.Header().Get("Content-Type"))
assert.Equal(t, "inline; filename=test.xml", resp.Header().Get("Content-Disposition"))
setting.MimeTypeMap.Map[".xml"] = "application/xml"
req = NewRequest(t, "GET", "/user2/repo2/raw/branch/master/test.xml")
resp = session.MakeRequest(t, req, http.StatusOK)
// non-text file don't have "charset"
assert.Equal(t, "application/xml", resp.Header().Get("Content-Type"))
})
}
+2 -1
View File
@@ -410,12 +410,13 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
}
}
func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) {
func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) {
t.Helper()
// FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names
decoder := json.NewDecoderCaseInsensitive(resp.Body)
require.NoError(t, decoder.Decode(v))
return v
}
func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) {
+39 -19
View File
@@ -23,9 +23,18 @@ import (
"github.com/stretchr/testify/require"
)
func TestOrgRepos(t *testing.T) {
func TestOrg(t *testing.T) {
defer tests.PrepareTestEnv(t)()
t.Run("OrgRepos", testOrgRepos)
t.Run("PrivateOrg", testPrivateOrg)
t.Run("LimitedOrg", testLimitedOrg)
t.Run("OrgMembers", testOrgMembers)
t.Run("OrgRestrictedUser", testOrgRestrictedUser)
t.Run("TeamSearch", testTeamSearch)
t.Run("OrgSettings", testOrgSettings)
}
func testOrgRepos(t *testing.T) {
var (
users = []string{"user1", "user2"}
cases = map[string][]string{
@@ -53,10 +62,8 @@ func TestOrgRepos(t *testing.T) {
}
}
func TestLimitedOrg(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// not logged in user
func testLimitedOrg(t *testing.T) {
// not logged-in user
req := NewRequest(t, "GET", "/limited_org")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org")
@@ -83,10 +90,8 @@ func TestLimitedOrg(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
}
func TestPrivateOrg(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// not logged in user
func testPrivateOrg(t *testing.T) {
// not logged-in user
req := NewRequest(t, "GET", "/privated_org")
MakeRequest(t, req, http.StatusNotFound)
req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org")
@@ -122,10 +127,8 @@ func TestPrivateOrg(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
}
func TestOrgMembers(t *testing.T) {
defer tests.PrepareTestEnv(t)()
// not logged in user
func testOrgMembers(t *testing.T) {
// not logged-in user
req := NewRequest(t, "GET", "/org/org25/members")
MakeRequest(t, req, http.StatusOK)
@@ -140,9 +143,7 @@ func TestOrgMembers(t *testing.T) {
session.MakeRequest(t, req, http.StatusOK)
}
func TestOrgRestrictedUser(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testOrgRestrictedUser(t *testing.T) {
// privated_org is a private org who has id 23
orgName := "privated_org"
@@ -200,9 +201,7 @@ func TestOrgRestrictedUser(t *testing.T) {
restrictedSession.MakeRequest(t, req, http.StatusOK)
}
func TestTeamSearch(t *testing.T) {
defer tests.PrepareTestEnv(t)()
func testTeamSearch(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
@@ -251,3 +250,24 @@ func TestTeamSearch(t *testing.T) {
assert.Len(t, teams, 1) // team permission is "write", so can write "code"
})
}
func testOrgSettings(t *testing.T) {
session := loginUser(t, "user2")
req := NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{
"full_name": "org3 new full name",
"email": "org3-new-email@example.com",
})
session.MakeRequest(t, req, http.StatusSeeOther)
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
assert.Equal(t, "org3 new full name", org.FullName)
assert.Equal(t, "org3-new-email@example.com", org.Email)
req = NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{
"email": "", // empty email means "clear email"
})
session.MakeRequest(t, req, http.StatusSeeOther)
org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
assert.Equal(t, "org3 new full name", org.FullName)
assert.Empty(t, org.Email)
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"net/http"
"testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestRepositoryVisibilityChange(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
t.Run("MakePrivateRequiresCorrectName", func(t *testing.T) {
// Wrong name should be rejected with a JSON error
req := NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{
"action": "visibility",
"private": "true",
"confirm_repo_name": "wrong-name",
})
resp := session.MakeRequest(t, req, http.StatusBadRequest)
assert.NotEmpty(t, test.ParseJSONError(resp.Body.Bytes()).ErrorMessage)
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.False(t, repo1.IsPrivate)
// Correct full name (owner/repo) should succeed with a JSON redirect
req = NewRequestWithValues(t, "POST", "/user2/repo1/settings", map[string]string{
"action": "visibility",
"private": "true",
"confirm_repo_name": "user2/repo1",
})
resp = session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
repo1 = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
assert.True(t, repo1.IsPrivate)
})
t.Run("MakePublicDoesNotRequireName", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", "/user2/repo2/settings", map[string]string{
"action": "visibility",
"private": "false",
})
resp := session.MakeRequest(t, req, http.StatusOK)
assert.NotEmpty(t, test.ParseJSONRedirect(resp.Body.Bytes()).Redirect)
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
assert.False(t, repo2.IsPrivate)
})
}
+83
View File
@@ -14,6 +14,7 @@ import (
"testing"
"time"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/repo"
@@ -1146,6 +1147,10 @@ func Test_WebhookWorkflowRun(t *testing.T) {
testWorkflowRunEventsOnCancellingAbandonedRun(t, webhookData, false)
},
},
{
name: "WorkflowRunOnStoppingEndlessTasksForMultipleRuns",
testFunc: testWorkflowRunOnStoppingEndlessTasksForMultipleRuns,
},
}
for _, obj := range testCases {
t.Run(obj.name, func(t *testing.T) {
@@ -1576,6 +1581,84 @@ jobs:
assert.Equal(t, "user2/"+repoName, webhookData.payloads[1].Repo.FullName)
}
func testWorkflowRunOnStoppingEndlessTasksForMultipleRuns(t *testing.T, webhookData *workflowRunWebhook) {
defer test.MockVariableValue(&setting.Actions.EndlessTaskTimeout, time.Second)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
repoName := "test-workflow-run-stop-endless-tasks"
testRepo := unittest.AssertExistsAndLoadBean(t, &repo.Repository{ID: createActionsTestRepo(t, token, repoName, false).ID})
testAPICreateWebhookForRepo(t, session, "user2", repoName, webhookData.URL, "workflow_run")
runners := make([]*mockRunner, 2)
for i := range runners {
runners[i] = newMockRunner()
runners[i].registerAsRepoRunner(t, "user2", repoName, fmt.Sprintf("mock-runner-%d", i), []string{"ubuntu-latest"}, false)
}
workflowPath1 := ".gitea/workflows/endless-1.yml"
workflowPath2 := ".gitea/workflows/endless-2.yml"
workflowContent1 := `name: endless-1
on:
push:
paths:
- '.gitea/workflows/endless-1.yml'
jobs:
job-1:
runs-on: ubuntu-latest
steps:
- run: echo 'job-1'
`
workflowContent2 := `name: endless-2
on:
push:
paths:
- '.gitea/workflows/endless-2.yml'
jobs:
job-2:
runs-on: ubuntu-latest
steps:
- run: echo 'job-2'
`
opts1 := getWorkflowCreateFileOptions(user2, testRepo.DefaultBranch, "create "+workflowPath1, workflowContent1)
createWorkflowFile(t, token, "user2", repoName, workflowPath1, opts1)
opts2 := getWorkflowCreateFileOptions(user2, testRepo.DefaultBranch, "create "+workflowPath2, workflowContent2)
createWorkflowFile(t, token, "user2", repoName, workflowPath2, opts2)
task1 := runners[0].fetchTask(t)
task2 := runners[1].fetchTask(t)
_, job1, _ := getTaskAndJobAndRunByTaskID(t, task1.Id)
_, job2, _ := getTaskAndJobAndRunByTaskID(t, task2.Id)
require.NotEqual(t, job1.RunID, job2.RunID)
initialRunEventsLen := len(webhookData.payloads)
time.Sleep(2 * time.Second)
require.NoError(t, actions.StopEndlessTasks(t.Context()))
require.Len(t, webhookData.payloads, initialRunEventsLen+2)
var completedRunIDs []int64
for _, payload := range webhookData.payloads[initialRunEventsLen:] {
assert.Equal(t, "completed", payload.Action)
assert.Equal(t, "completed", payload.WorkflowRun.Status)
completedRunIDs = append(completedRunIDs, payload.WorkflowRun.ID)
}
assert.Len(t, completedRunIDs, 2)
assert.Contains(t, completedRunIDs, job1.RunID)
assert.Contains(t, completedRunIDs, job2.RunID)
run1 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job1.RunID})
run2 := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: job2.RunID})
assert.Equal(t, actions_model.StatusFailure, run1.Status)
assert.Equal(t, actions_model.StatusFailure, run2.Status)
}
func testWebhookWorkflowRun(t *testing.T, webhookData *workflowRunWebhook) {
// 1. create a new webhook with special webhook for repo1
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})