mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 11:35:03 +01:00 
			
		
		
		
	* repository deletion, delete ephemeral runners with active tasks as well skips regular cleanup * user deletion, delete ephemeral runners with active tasks as well skips regular cleanup * delete ephemeral runners once status changes to done * You no longer see used ephemeral runners after the task is done * if you see one the cron job takes care of it
		
			
				
	
	
		
			343 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			343 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2025 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package integration
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"slices"
 | |
| 	"testing"
 | |
| 
 | |
| 	auth_model "code.gitea.io/gitea/models/auth"
 | |
| 	api "code.gitea.io/gitea/modules/structs"
 | |
| 	"code.gitea.io/gitea/tests"
 | |
| 
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| )
 | |
| 
 | |
| func TestAPIActionsRunner(t *testing.T) {
 | |
| 	t.Run("AdminRunner", testActionsRunnerAdmin)
 | |
| 	t.Run("UserRunner", testActionsRunnerUser)
 | |
| 	t.Run("OwnerRunner", testActionsRunnerOwner)
 | |
| 	t.Run("RepoRunner", testActionsRunnerRepo)
 | |
| }
 | |
| 
 | |
| func testActionsRunnerAdmin(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 	adminUsername := "user1"
 | |
| 	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeWriteAdmin)
 | |
| 	req := NewRequest(t, "POST", "/api/v1/admin/actions/runners/registration-token").AddTokenAuth(token)
 | |
| 	tokenResp := MakeRequest(t, req, http.StatusOK)
 | |
| 	var registrationToken struct {
 | |
| 		Token string `json:"token"`
 | |
| 	}
 | |
| 	DecodeJSON(t, tokenResp, ®istrationToken)
 | |
| 	assert.NotEmpty(t, registrationToken.Token)
 | |
| 
 | |
| 	req = NewRequest(t, "GET", "/api/v1/admin/actions/runners").AddTokenAuth(token)
 | |
| 	runnerListResp := MakeRequest(t, req, http.StatusOK)
 | |
| 	runnerList := api.ActionRunnersResponse{}
 | |
| 	DecodeJSON(t, runnerListResp, &runnerList)
 | |
| 
 | |
| 	idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34349 })
 | |
| 	require.NotEqual(t, -1, idx)
 | |
| 	expectedRunner := runnerList.Entries[idx]
 | |
| 	assert.Equal(t, "runner_to_be_deleted", expectedRunner.Name)
 | |
| 	assert.False(t, expectedRunner.Ephemeral)
 | |
| 	assert.Len(t, expectedRunner.Labels, 2)
 | |
| 	assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
 | |
| 	assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
 | |
| 
 | |
| 	// Verify all returned runners can be requested and deleted
 | |
| 	for _, runnerEntry := range runnerList.Entries {
 | |
| 		// Verify get the runner by id
 | |
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
 | |
| 		runnerResp := MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 		runner := api.ActionRunner{}
 | |
| 		DecodeJSON(t, runnerResp, &runner)
 | |
| 
 | |
| 		assert.Equal(t, runnerEntry.Name, runner.Name)
 | |
| 		assert.Equal(t, runnerEntry.ID, runner.ID)
 | |
| 		assert.Equal(t, runnerEntry.Ephemeral, runner.Ephemeral)
 | |
| 		assert.ElementsMatch(t, runnerEntry.Labels, runner.Labels)
 | |
| 
 | |
| 		req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNoContent)
 | |
| 
 | |
| 		// Verify runner deletion
 | |
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/admin/actions/runners/%d", runnerEntry.ID)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNotFound)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func testActionsRunnerUser(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 	userUsername := "user1"
 | |
| 	token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteUser)
 | |
| 	req := NewRequest(t, "POST", "/api/v1/user/actions/runners/registration-token").AddTokenAuth(token)
 | |
| 	tokenResp := MakeRequest(t, req, http.StatusOK)
 | |
| 	var registrationToken struct {
 | |
| 		Token string `json:"token"`
 | |
| 	}
 | |
| 	DecodeJSON(t, tokenResp, ®istrationToken)
 | |
| 	assert.NotEmpty(t, registrationToken.Token)
 | |
| 
 | |
| 	req = NewRequest(t, "GET", "/api/v1/user/actions/runners").AddTokenAuth(token)
 | |
| 	runnerListResp := MakeRequest(t, req, http.StatusOK)
 | |
| 	runnerList := api.ActionRunnersResponse{}
 | |
| 	DecodeJSON(t, runnerListResp, &runnerList)
 | |
| 
 | |
| 	assert.Len(t, runnerList.Entries, 1)
 | |
| 	assert.Equal(t, "runner_to_be_deleted-user", runnerList.Entries[0].Name)
 | |
| 	assert.Equal(t, int64(34346), runnerList.Entries[0].ID)
 | |
| 	assert.False(t, runnerList.Entries[0].Ephemeral)
 | |
| 	assert.Len(t, runnerList.Entries[0].Labels, 2)
 | |
| 	assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
 | |
| 	assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
 | |
| 
 | |
| 	// Verify get the runner by id
 | |
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
 | |
| 	runnerResp := MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 	runner := api.ActionRunner{}
 | |
| 	DecodeJSON(t, runnerResp, &runner)
 | |
| 
 | |
| 	assert.Equal(t, "runner_to_be_deleted-user", runner.Name)
 | |
| 	assert.Equal(t, int64(34346), runner.ID)
 | |
| 	assert.False(t, runner.Ephemeral)
 | |
| 	assert.Len(t, runner.Labels, 2)
 | |
| 	assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
 | |
| 	assert.Equal(t, "linux", runner.Labels[1].Name)
 | |
| 
 | |
| 	// Verify delete the runner by id
 | |
| 	req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
 | |
| 	MakeRequest(t, req, http.StatusNoContent)
 | |
| 
 | |
| 	// Verify runner deletion
 | |
| 	req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/user/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
 | |
| 	MakeRequest(t, req, http.StatusNotFound)
 | |
| }
 | |
| 
 | |
| func testActionsRunnerOwner(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	t.Run("GetRunner", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
 | |
| 		// Verify get the runner by id with read scope
 | |
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
 | |
| 		runnerResp := MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 		runner := api.ActionRunner{}
 | |
| 		DecodeJSON(t, runnerResp, &runner)
 | |
| 
 | |
| 		assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
 | |
| 		assert.Equal(t, int64(34347), runner.ID)
 | |
| 		assert.False(t, runner.Ephemeral)
 | |
| 		assert.Len(t, runner.Labels, 2)
 | |
| 		assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
 | |
| 		assert.Equal(t, "linux", runner.Labels[1].Name)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("Access", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteOrganization)
 | |
| 		req := NewRequest(t, "POST", "/api/v1/orgs/org3/actions/runners/registration-token").AddTokenAuth(token)
 | |
| 		tokenResp := MakeRequest(t, req, http.StatusOK)
 | |
| 		var registrationToken struct {
 | |
| 			Token string `json:"token"`
 | |
| 		}
 | |
| 		DecodeJSON(t, tokenResp, ®istrationToken)
 | |
| 		assert.NotEmpty(t, registrationToken.Token)
 | |
| 
 | |
| 		req = NewRequest(t, "GET", "/api/v1/orgs/org3/actions/runners").AddTokenAuth(token)
 | |
| 		runnerListResp := MakeRequest(t, req, http.StatusOK)
 | |
| 		runnerList := api.ActionRunnersResponse{}
 | |
| 		DecodeJSON(t, runnerListResp, &runnerList)
 | |
| 
 | |
| 		idx := slices.IndexFunc(runnerList.Entries, func(e *api.ActionRunner) bool { return e.ID == 34347 })
 | |
| 		require.NotEqual(t, -1, idx)
 | |
| 		expectedRunner := runnerList.Entries[idx]
 | |
| 
 | |
| 		require.NotNil(t, expectedRunner)
 | |
| 		assert.Equal(t, "runner_to_be_deleted-org", expectedRunner.Name)
 | |
| 		assert.Equal(t, int64(34347), expectedRunner.ID)
 | |
| 		assert.False(t, expectedRunner.Ephemeral)
 | |
| 		assert.Len(t, expectedRunner.Labels, 2)
 | |
| 		assert.Equal(t, "runner_to_be_deleted", expectedRunner.Labels[0].Name)
 | |
| 		assert.Equal(t, "linux", expectedRunner.Labels[1].Name)
 | |
| 
 | |
| 		// Verify get the runner by id
 | |
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
 | |
| 		runnerResp := MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 		runner := api.ActionRunner{}
 | |
| 		DecodeJSON(t, runnerResp, &runner)
 | |
| 
 | |
| 		assert.Equal(t, "runner_to_be_deleted-org", runner.Name)
 | |
| 		assert.Equal(t, int64(34347), runner.ID)
 | |
| 		assert.False(t, runner.Ephemeral)
 | |
| 		assert.Len(t, runner.Labels, 2)
 | |
| 		assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
 | |
| 		assert.Equal(t, "linux", runner.Labels[1].Name)
 | |
| 
 | |
| 		// Verify delete the runner by id
 | |
| 		req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNoContent)
 | |
| 
 | |
| 		// Verify runner deletion
 | |
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", expectedRunner.ID)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNotFound)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("DeleteReadScopeForbidden", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
 | |
| 
 | |
| 		// Verify delete the runner by id is forbidden with read scope
 | |
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusForbidden)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("GetRepoScopeForbidden", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
 | |
| 		// Verify get the runner by id with read scope
 | |
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34347)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusForbidden)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("GetAdminRunner", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
 | |
| 		// Verify get a runner by id of different entity is not found
 | |
| 		// runner.EditableInContext(ownerID, repoID) false
 | |
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34349)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNotFound)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("DeleteAdminRunner", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteOrganization)
 | |
| 		// Verify delete a runner by id of different entity is not found
 | |
| 		// runner.EditableInContext(ownerID, repoID) false
 | |
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/orgs/org3/actions/runners/%d", 34349)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNotFound)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func testActionsRunnerRepo(t *testing.T) {
 | |
| 	defer tests.PrepareTestEnv(t)()
 | |
| 
 | |
| 	t.Run("GetRunner", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
 | |
| 		// Verify get the runner by id with read scope
 | |
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
 | |
| 		runnerResp := MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 		runner := api.ActionRunner{}
 | |
| 		DecodeJSON(t, runnerResp, &runner)
 | |
| 
 | |
| 		assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
 | |
| 		assert.Equal(t, int64(34348), runner.ID)
 | |
| 		assert.False(t, runner.Ephemeral)
 | |
| 		assert.Len(t, runner.Labels, 2)
 | |
| 		assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
 | |
| 		assert.Equal(t, "linux", runner.Labels[1].Name)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("Access", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
 | |
| 		req := NewRequest(t, "POST", "/api/v1/repos/user2/repo1/actions/runners/registration-token").AddTokenAuth(token)
 | |
| 		tokenResp := MakeRequest(t, req, http.StatusOK)
 | |
| 		var registrationToken struct {
 | |
| 			Token string `json:"token"`
 | |
| 		}
 | |
| 		DecodeJSON(t, tokenResp, ®istrationToken)
 | |
| 		assert.NotEmpty(t, registrationToken.Token)
 | |
| 
 | |
| 		req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/actions/runners").AddTokenAuth(token)
 | |
| 		runnerListResp := MakeRequest(t, req, http.StatusOK)
 | |
| 		runnerList := api.ActionRunnersResponse{}
 | |
| 		DecodeJSON(t, runnerListResp, &runnerList)
 | |
| 
 | |
| 		assert.Len(t, runnerList.Entries, 1)
 | |
| 		assert.Equal(t, "runner_to_be_deleted-repo1", runnerList.Entries[0].Name)
 | |
| 		assert.Equal(t, int64(34348), runnerList.Entries[0].ID)
 | |
| 		assert.False(t, runnerList.Entries[0].Ephemeral)
 | |
| 		assert.Len(t, runnerList.Entries[0].Labels, 2)
 | |
| 		assert.Equal(t, "runner_to_be_deleted", runnerList.Entries[0].Labels[0].Name)
 | |
| 		assert.Equal(t, "linux", runnerList.Entries[0].Labels[1].Name)
 | |
| 
 | |
| 		// Verify get the runner by id
 | |
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
 | |
| 		runnerResp := MakeRequest(t, req, http.StatusOK)
 | |
| 
 | |
| 		runner := api.ActionRunner{}
 | |
| 		DecodeJSON(t, runnerResp, &runner)
 | |
| 
 | |
| 		assert.Equal(t, "runner_to_be_deleted-repo1", runner.Name)
 | |
| 		assert.Equal(t, int64(34348), runner.ID)
 | |
| 		assert.False(t, runner.Ephemeral)
 | |
| 		assert.Len(t, runner.Labels, 2)
 | |
| 		assert.Equal(t, "runner_to_be_deleted", runner.Labels[0].Name)
 | |
| 		assert.Equal(t, "linux", runner.Labels[1].Name)
 | |
| 
 | |
| 		// Verify delete the runner by id
 | |
| 		req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNoContent)
 | |
| 
 | |
| 		// Verify runner deletion
 | |
| 		req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", runnerList.Entries[0].ID)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNotFound)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("DeleteReadScopeForbidden", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
 | |
| 
 | |
| 		// Verify delete the runner by id is forbidden with read scope
 | |
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusForbidden)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("GetOrganizationScopeForbidden", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadOrganization)
 | |
| 		// Verify get the runner by id with read scope
 | |
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34348)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusForbidden)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("GetAdminRunnerNotFound", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeReadRepository)
 | |
| 		// Verify get a runner by id of different entity is not found
 | |
| 		// runner.EditableInContext(ownerID, repoID) false
 | |
| 		req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNotFound)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("DeleteAdminRunnerNotFound", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
 | |
| 		// Verify delete a runner by id of different entity is not found
 | |
| 		// runner.EditableInContext(ownerID, repoID) false
 | |
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 34349)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNotFound)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("DeleteAdminRunnerNotFoundUnknownID", func(t *testing.T) {
 | |
| 		userUsername := "user2"
 | |
| 		token := getUserToken(t, userUsername, auth_model.AccessTokenScopeWriteRepository)
 | |
| 		// Verify delete a runner by unknown id is not found
 | |
| 		req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/user2/repo1/actions/runners/%d", 4384797347934)).AddTokenAuth(token)
 | |
| 		MakeRequest(t, req, http.StatusNotFound)
 | |
| 	})
 | |
| }
 |