mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-10-29 13:31:20 +00:00 
			
		
		
		
	Fix `Uploaded artifacts should be overwritten` https://github.com/go-gitea/gitea/issues/28549 When upload different content to uploaded artifact, it checks that content size is not match in db record with previous artifact size, then the new artifact is refused. Now if it finds uploading content size is not matching db record when receiving chunks, it updates db records to follow the latest size value.
		
			
				
	
	
		
			378 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			378 lines
		
	
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2023 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package integration
 | |
| 
 | |
| import (
 | |
| 	"net/http"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"code.gitea.io/gitea/tests"
 | |
| 
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| )
 | |
| 
 | |
| type uploadArtifactResponse struct {
 | |
| 	FileContainerResourceURL string `json:"fileContainerResourceUrl"`
 | |
| }
 | |
| 
 | |
| type getUploadArtifactRequest struct {
 | |
| 	Type          string
 | |
| 	Name          string
 | |
| 	RetentionDays int64
 | |
| }
 | |
| 
 | |
| func TestActionsArtifactUploadSingleFile(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	// acquire artifact upload url
 | |
| 	req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
 | |
| 		Type: "actions_storage",
 | |
| 		Name: "artifact",
 | |
| 	}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp := MakeRequest(t, req, http.StatusOK)
 | |
| 	var uploadResp uploadArtifactResponse
 | |
| 	DecodeJSON(t, resp, &uploadResp)
 | |
| 	assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | |
| 
 | |
| 	// get upload url
 | |
| 	idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 	url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc.txt"
 | |
| 
 | |
| 	// upload artifact chunk
 | |
| 	body := strings.Repeat("A", 1024)
 | |
| 	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
 | |
| 		SetHeader("Content-Range", "bytes 0-1023/1024").
 | |
| 		SetHeader("x-tfs-filelength", "1024").
 | |
| 		SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
 | |
| 	MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 	t.Logf("Create artifact confirm")
 | |
| 
 | |
| 	// confirm artifact upload
 | |
| 	req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact").
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	MakeRequest(t, req, http.StatusOK)
 | |
| }
 | |
| 
 | |
| func TestActionsArtifactUploadInvalidHash(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	// artifact id 54321 not exist
 | |
| 	url := "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts/8e5b948a454515dbabfc7eb718ddddddd/upload?itemPath=artifact/abc.txt"
 | |
| 	body := strings.Repeat("A", 1024)
 | |
| 	req := NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
 | |
| 		SetHeader("Content-Range", "bytes 0-1023/1024").
 | |
| 		SetHeader("x-tfs-filelength", "1024").
 | |
| 		SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
 | |
| 	resp := MakeRequest(t, req, http.StatusBadRequest)
 | |
| 	assert.Contains(t, resp.Body.String(), "Invalid artifact hash")
 | |
| }
 | |
| 
 | |
| func TestActionsArtifactConfirmUploadWithoutName(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	req := NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp := MakeRequest(t, req, http.StatusBadRequest)
 | |
| 	assert.Contains(t, resp.Body.String(), "artifact name is empty")
 | |
| }
 | |
| 
 | |
| func TestActionsArtifactUploadWithoutToken(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/1/artifacts", nil)
 | |
| 	MakeRequest(t, req, http.StatusUnauthorized)
 | |
| }
 | |
| 
 | |
| type (
 | |
| 	listArtifactsResponseItem struct {
 | |
| 		Name                     string `json:"name"`
 | |
| 		FileContainerResourceURL string `json:"fileContainerResourceUrl"`
 | |
| 	}
 | |
| 	listArtifactsResponse struct {
 | |
| 		Count int64                       `json:"count"`
 | |
| 		Value []listArtifactsResponseItem `json:"value"`
 | |
| 	}
 | |
| 	downloadArtifactResponseItem struct {
 | |
| 		Path            string `json:"path"`
 | |
| 		ItemType        string `json:"itemType"`
 | |
| 		ContentLocation string `json:"contentLocation"`
 | |
| 	}
 | |
| 	downloadArtifactResponse struct {
 | |
| 		Value []downloadArtifactResponseItem `json:"value"`
 | |
| 	}
 | |
| )
 | |
| 
 | |
| func TestActionsArtifactDownload(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp := MakeRequest(t, req, http.StatusOK)
 | |
| 	var listResp listArtifactsResponse
 | |
| 	DecodeJSON(t, resp, &listResp)
 | |
| 	assert.Equal(t, int64(1), listResp.Count)
 | |
| 	assert.Equal(t, "artifact", listResp.Value[0].Name)
 | |
| 	assert.Contains(t, listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | |
| 
 | |
| 	idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 	url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact"
 | |
| 	req = NewRequest(t, "GET", url).
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp = MakeRequest(t, req, http.StatusOK)
 | |
| 	var downloadResp downloadArtifactResponse
 | |
| 	DecodeJSON(t, resp, &downloadResp)
 | |
| 	assert.Len(t, downloadResp.Value, 1)
 | |
| 	assert.Equal(t, "artifact/abc.txt", downloadResp.Value[0].Path)
 | |
| 	assert.Equal(t, "file", downloadResp.Value[0].ItemType)
 | |
| 	assert.Contains(t, downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | |
| 
 | |
| 	idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 	url = downloadResp.Value[0].ContentLocation[idx:]
 | |
| 	req = NewRequest(t, "GET", url).
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp = MakeRequest(t, req, http.StatusOK)
 | |
| 	body := strings.Repeat("A", 1024)
 | |
| 	assert.Equal(t, resp.Body.String(), body)
 | |
| }
 | |
| 
 | |
| func TestActionsArtifactUploadMultipleFile(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	const testArtifactName = "multi-files"
 | |
| 
 | |
| 	// acquire artifact upload url
 | |
| 	req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
 | |
| 		Type: "actions_storage",
 | |
| 		Name: testArtifactName,
 | |
| 	}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp := MakeRequest(t, req, http.StatusOK)
 | |
| 	var uploadResp uploadArtifactResponse
 | |
| 	DecodeJSON(t, resp, &uploadResp)
 | |
| 	assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | |
| 
 | |
| 	type uploadingFile struct {
 | |
| 		Path    string
 | |
| 		Content string
 | |
| 		MD5     string
 | |
| 	}
 | |
| 
 | |
| 	files := []uploadingFile{
 | |
| 		{
 | |
| 			Path:    "abc.txt",
 | |
| 			Content: strings.Repeat("A", 1024),
 | |
| 			MD5:     "1HsSe8LeLWh93ILaw1TEFQ==",
 | |
| 		},
 | |
| 		{
 | |
| 			Path:    "xyz/def.txt",
 | |
| 			Content: strings.Repeat("B", 1024),
 | |
| 			MD5:     "6fgADK/7zjadf+6cB9Q1CQ==",
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, f := range files {
 | |
| 		// get upload url
 | |
| 		idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 		url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=" + testArtifactName + "/" + f.Path
 | |
| 
 | |
| 		// upload artifact chunk
 | |
| 		req = NewRequestWithBody(t, "PUT", url, strings.NewReader(f.Content)).
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
 | |
| 			SetHeader("Content-Range", "bytes 0-1023/1024").
 | |
| 			SetHeader("x-tfs-filelength", "1024").
 | |
| 			SetHeader("x-actions-results-md5", f.MD5) // base64(md5(body))
 | |
| 		MakeRequest(t, req, http.StatusOK)
 | |
| 	}
 | |
| 
 | |
| 	t.Logf("Create artifact confirm")
 | |
| 
 | |
| 	// confirm artifact upload
 | |
| 	req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName="+testArtifactName).
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	MakeRequest(t, req, http.StatusOK)
 | |
| }
 | |
| 
 | |
| func TestActionsArtifactDownloadMultiFiles(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	const testArtifactName = "multi-files"
 | |
| 
 | |
| 	req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp := MakeRequest(t, req, http.StatusOK)
 | |
| 	var listResp listArtifactsResponse
 | |
| 	DecodeJSON(t, resp, &listResp)
 | |
| 	assert.Equal(t, int64(2), listResp.Count)
 | |
| 
 | |
| 	var fileContainerResourceURL string
 | |
| 	for _, v := range listResp.Value {
 | |
| 		if v.Name == testArtifactName {
 | |
| 			fileContainerResourceURL = v.FileContainerResourceURL
 | |
| 			break
 | |
| 		}
 | |
| 	}
 | |
| 	assert.Contains(t, fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | |
| 
 | |
| 	idx := strings.Index(fileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 	url := fileContainerResourceURL[idx+1:] + "?itemPath=" + testArtifactName
 | |
| 	req = NewRequest(t, "GET", url).
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp = MakeRequest(t, req, http.StatusOK)
 | |
| 	var downloadResp downloadArtifactResponse
 | |
| 	DecodeJSON(t, resp, &downloadResp)
 | |
| 	assert.Len(t, downloadResp.Value, 2)
 | |
| 
 | |
| 	downloads := [][]string{{"multi-files/abc.txt", "A"}, {"multi-files/xyz/def.txt", "B"}}
 | |
| 	for _, v := range downloadResp.Value {
 | |
| 		var bodyChar string
 | |
| 		var path string
 | |
| 		for _, d := range downloads {
 | |
| 			if v.Path == d[0] {
 | |
| 				path = d[0]
 | |
| 				bodyChar = d[1]
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		value := v
 | |
| 		assert.Equal(t, path, value.Path)
 | |
| 		assert.Equal(t, "file", value.ItemType)
 | |
| 		assert.Contains(t, value.ContentLocation, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | |
| 
 | |
| 		idx = strings.Index(value.ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 		url = value.ContentLocation[idx:]
 | |
| 		req = NewRequest(t, "GET", url).
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		resp = MakeRequest(t, req, http.StatusOK)
 | |
| 		body := strings.Repeat(bodyChar, 1024)
 | |
| 		assert.Equal(t, resp.Body.String(), body)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestActionsArtifactUploadWithRetentionDays(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	// acquire artifact upload url
 | |
| 	req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
 | |
| 		Type:          "actions_storage",
 | |
| 		Name:          "artifact-retention-days",
 | |
| 		RetentionDays: 9,
 | |
| 	}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	resp := MakeRequest(t, req, http.StatusOK)
 | |
| 	var uploadResp uploadArtifactResponse
 | |
| 	DecodeJSON(t, resp, &uploadResp)
 | |
| 	assert.Contains(t, uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts")
 | |
| 	assert.Contains(t, uploadResp.FileContainerResourceURL, "?retentionDays=9")
 | |
| 
 | |
| 	// get upload url
 | |
| 	idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 	url := uploadResp.FileContainerResourceURL[idx:] + "&itemPath=artifact-retention-days/abc.txt"
 | |
| 
 | |
| 	// upload artifact chunk
 | |
| 	body := strings.Repeat("A", 1024)
 | |
| 	req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
 | |
| 		SetHeader("Content-Range", "bytes 0-1023/1024").
 | |
| 		SetHeader("x-tfs-filelength", "1024").
 | |
| 		SetHeader("x-actions-results-md5", "1HsSe8LeLWh93ILaw1TEFQ==") // base64(md5(body))
 | |
| 	MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 	t.Logf("Create artifact confirm")
 | |
| 
 | |
| 	// confirm artifact upload
 | |
| 	req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact-retention-days").
 | |
| 		AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 	MakeRequest(t, req, http.StatusOK)
 | |
| }
 | |
| 
 | |
| func TestActionsArtifactOverwrite(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	{
 | |
| 		// download old artifact uploaded by tests above, it should 1024 A
 | |
| 		req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		resp := MakeRequest(t, req, http.StatusOK)
 | |
| 		var listResp listArtifactsResponse
 | |
| 		DecodeJSON(t, resp, &listResp)
 | |
| 
 | |
| 		idx := strings.Index(listResp.Value[0].FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 		url := listResp.Value[0].FileContainerResourceURL[idx+1:] + "?itemPath=artifact"
 | |
| 		req = NewRequest(t, "GET", url).
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		resp = MakeRequest(t, req, http.StatusOK)
 | |
| 		var downloadResp downloadArtifactResponse
 | |
| 		DecodeJSON(t, resp, &downloadResp)
 | |
| 
 | |
| 		idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 		url = downloadResp.Value[0].ContentLocation[idx:]
 | |
| 		req = NewRequest(t, "GET", url).
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		resp = MakeRequest(t, req, http.StatusOK)
 | |
| 		body := strings.Repeat("A", 1024)
 | |
| 		assert.Equal(t, resp.Body.String(), body)
 | |
| 	}
 | |
| 
 | |
| 	{
 | |
| 		// upload same artifact, it uses 4096 B
 | |
| 		req := NewRequestWithJSON(t, "POST", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts", getUploadArtifactRequest{
 | |
| 			Type: "actions_storage",
 | |
| 			Name: "artifact",
 | |
| 		}).AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		resp := MakeRequest(t, req, http.StatusOK)
 | |
| 		var uploadResp uploadArtifactResponse
 | |
| 		DecodeJSON(t, resp, &uploadResp)
 | |
| 
 | |
| 		idx := strings.Index(uploadResp.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 		url := uploadResp.FileContainerResourceURL[idx:] + "?itemPath=artifact/abc.txt"
 | |
| 		body := strings.Repeat("B", 4096)
 | |
| 		req = NewRequestWithBody(t, "PUT", url, strings.NewReader(body)).
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a").
 | |
| 			SetHeader("Content-Range", "bytes 0-4095/4096").
 | |
| 			SetHeader("x-tfs-filelength", "4096").
 | |
| 			SetHeader("x-actions-results-md5", "wUypcJFeZCK5T6r4lfqzqg==") // base64(md5(body))
 | |
| 		MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 		// confirm artifact upload
 | |
| 		req = NewRequest(t, "PATCH", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts?artifactName=artifact").
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		MakeRequest(t, req, http.StatusOK)
 | |
| 	}
 | |
| 
 | |
| 	{
 | |
| 		// download artifact again, it should 4096 B
 | |
| 		req := NewRequest(t, "GET", "/api/actions_pipeline/_apis/pipelines/workflows/791/artifacts").
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		resp := MakeRequest(t, req, http.StatusOK)
 | |
| 		var listResp listArtifactsResponse
 | |
| 		DecodeJSON(t, resp, &listResp)
 | |
| 
 | |
| 		var uploadedItem listArtifactsResponseItem
 | |
| 		for _, item := range listResp.Value {
 | |
| 			if item.Name == "artifact" {
 | |
| 				uploadedItem = item
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		assert.Equal(t, uploadedItem.Name, "artifact")
 | |
| 
 | |
| 		idx := strings.Index(uploadedItem.FileContainerResourceURL, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 		url := uploadedItem.FileContainerResourceURL[idx+1:] + "?itemPath=artifact"
 | |
| 		req = NewRequest(t, "GET", url).
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		resp = MakeRequest(t, req, http.StatusOK)
 | |
| 		var downloadResp downloadArtifactResponse
 | |
| 		DecodeJSON(t, resp, &downloadResp)
 | |
| 
 | |
| 		idx = strings.Index(downloadResp.Value[0].ContentLocation, "/api/actions_pipeline/_apis/pipelines/")
 | |
| 		url = downloadResp.Value[0].ContentLocation[idx:]
 | |
| 		req = NewRequest(t, "GET", url).
 | |
| 			AddTokenAuth("8061e833a55f6fc0157c98b883e91fcfeeb1a71a")
 | |
| 		resp = MakeRequest(t, req, http.StatusOK)
 | |
| 		body := strings.Repeat("B", 4096)
 | |
| 		assert.Equal(t, resp.Body.String(), body)
 | |
| 	}
 | |
| }
 |