feat: allow renaming and replacing secrets (#11732)

So far, Forgejo's UI only allowed to create Forgejo Actions secrets. But renaming or replacing their value wasn't possible. With this change, users can do both. The existing secret value is never revealed for security reasons.

Additionally, a confusing behaviour is removed. If a user created a new secret whose name matched an existing secret, the existing secret was silently updated. That does no longer happen. The new secret is rejected instead.

Resolves https://codeberg.org/forgejo/forgejo/issues/5707.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests for Go changes

(can be removed for JavaScript changes)

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I ran...
  - [x] `make pr-go` before pushing

### Tests for JavaScript changes

(can be removed for Go changes)

- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change.
- [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change.

*The decision if the pull request will be shown in the release notes is up to the mergers / release team.*

The content of the `release-notes/<pull request number>.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11732
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
This commit is contained in:
Andreas Ahlenstorf 2026-03-23 03:30:02 +01:00 committed by Mathieu Fenniak
parent ce1c0dc2cc
commit bdbd0b5622
18 changed files with 776 additions and 251 deletions

View file

@ -0,0 +1,23 @@
- id: 637340
name: TEST_SECRET
owner_id: 3
repo_id: 0
# very secret
data: 0x0e17d357077de0c1f72e9d87e1899c8026a207fcbbc0f1a2a79d0cb305da39fa3e9fe201b085e963fa1f9eeb59b4ff9ee6d748
created_unix: 1773692671
- id: 637341
name: ANOTHER_SECRET
owner_id: 0
repo_id: 62 # user2/test_workflows
# also very secret
data: 0xb697cd4a66b02bc36d0afddcb6f158d7602f6cf0b0d31a0c96e178469e8b66babccb30b95e01c84824add04c5bfe91b9b773a21a78088a3e
created_unix: 1773692672
- id: 637342
name: TEST_SECRET
owner_id: 1
repo_id: 0
# super secret
data: 0xd97d8cb662c07ca94953a388bc93209f713281a8b0d25499359cbd03a1fbe565a2a0ba183b8290fc110ee6b1c6437c569451e0c3
created_unix: 1773692673

View file

@ -6,6 +6,7 @@ package secret
import (
"context"
"fmt"
"regexp"
"strings"
"forgejo.org/models/db"
@ -17,6 +18,13 @@ import (
"xorm.io/builder"
)
var (
namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$")
forbiddenPrefixPattern = regexp.MustCompile("(?i)^FORGEJO_|GITEA_|GITHUB_")
ErrInvalidName = util.NewInvalidArgumentErrorf("invalid secret name")
)
// Secret represents a secret
//
// It can be:
@ -63,6 +71,9 @@ func InsertEncryptedSecret(ctx context.Context, ownerID, repoID int64, name, dat
if ownerID == 0 && repoID == 0 {
return nil, fmt.Errorf("%w: ownerID and repoID cannot be both zero, global secrets are not supported", util.ErrInvalidArgument)
}
if err := ValidateName(name); err != nil {
return nil, err
}
secret := &Secret{
OwnerID: ownerID,
@ -129,6 +140,46 @@ func (s *Secret) GetDecryptedData() (string, error) {
return string(v), nil
}
func GetSecretByID(ctx context.Context, ownerID, repoID, id int64) (*Secret, error) {
query := db.GetEngine(ctx).Where("id=?", id)
if repoID > 0 {
query = query.And(builder.Eq{"repo_id": repoID})
} else if ownerID > 0 {
query = query.And(builder.Eq{"owner_id": ownerID})
} else {
return nil, fmt.Errorf("ownerID and repoID cannot be simultaneously 0")
}
var secret Secret
has, err := query.Get(&secret)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("secret with ID %d: %w", id, util.ErrNotExist)
}
return &secret, nil
}
func UpdateSecret(ctx context.Context, secret *Secret, columns ...string) error {
e := db.GetEngine(ctx)
if err := ValidateName(secret.Name); err != nil {
return err
}
secret.Name = strings.ToUpper(secret.Name)
var err error
if len(columns) == 0 {
_, err = e.ID(secret.ID).AllCols().Update(secret)
} else {
_, err = e.ID(secret.ID).Cols(columns...).Update(secret)
}
return err
}
func FetchActionSecrets(ctx context.Context, ownerID, repoID int64) (map[string]string, error) {
secrets := map[string]string{}
@ -154,3 +205,10 @@ func FetchActionSecrets(ctx context.Context, ownerID, repoID int64) (map[string]
return secrets, nil
}
func ValidateName(name string) error {
if !namePattern.MatchString(name) || forbiddenPrefixPattern.MatchString(name) {
return ErrInvalidName
}
return nil
}

View file

@ -4,6 +4,7 @@
package secret
import (
"strings"
"testing"
"forgejo.org/models/unittest"
@ -83,6 +84,11 @@ func TestInsertEncryptedSecret(t *testing.T) {
})
})
t.Run("Rejects invalid name", func(t *testing.T) {
_, err := InsertEncryptedSecret(t.Context(), 2, 0, "invalid name", "some secret")
require.ErrorContains(t, err, "invalid secret name")
})
t.Run("FetchActionSecrets", func(t *testing.T) {
secrets, err := FetchActionSecrets(t.Context(), 2, 1)
require.NoError(t, err)
@ -123,3 +129,169 @@ func TestSecretGetDecryptedData(t *testing.T) {
assert.ErrorContains(t, err, "unable to decrypt secret[id=495,name=\"A_SECRET\"]")
})
}
func TestSecretGetSecretByID(t *testing.T) {
defer unittest.OverrideFixtures("models/secret/TestSecretGetSecretByID")()
require.NoError(t, unittest.PrepareTestDatabase())
testCases := []struct {
name string
ownerID int64
repoID int64
id int64
expectedName string
expectedData string
expectedError string
}{
{
name: "Organization secret",
ownerID: 3,
repoID: 0,
id: 637340,
expectedName: "TEST_SECRET",
expectedData: "very secret",
},
{
name: "Owner mismatch",
ownerID: 4,
repoID: 0,
id: 637340,
expectedError: "secret with ID 637340: resource does not exist",
},
{
name: "Repository mismatch",
ownerID: 0,
repoID: 1,
id: 637340,
expectedError: "secret with ID 637340: resource does not exist",
},
{
name: "Repository secret",
ownerID: 0,
repoID: 62,
id: 637341,
expectedName: "ANOTHER_SECRET",
expectedData: "also very secret",
},
{
name: "Unsupported instance secret",
ownerID: 0,
repoID: 0,
id: 637341,
expectedError: "ownerID and repoID cannot be simultaneously 0",
},
{
name: "User secret",
ownerID: 1,
repoID: 0,
id: 637342,
expectedName: "TEST_SECRET",
expectedData: "super secret",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
secret, err := GetSecretByID(t.Context(), testCase.ownerID, testCase.repoID, testCase.id)
if testCase.expectedError != "" {
assert.ErrorContains(t, err, testCase.expectedError)
} else {
require.NoError(t, err)
assert.Equal(t, testCase.id, secret.ID)
assert.Equal(t, testCase.ownerID, secret.OwnerID)
assert.Equal(t, testCase.repoID, secret.RepoID)
assert.Equal(t, testCase.expectedName, secret.Name)
data, err := secret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, testCase.expectedData, data)
}
})
}
}
func TestSecretUpdateSecret(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
secret, err := InsertEncryptedSecret(t.Context(), 2, 0, "a_secret", "very secret")
require.NoError(t, err)
secret.Name = "new_name"
secret.SetData("also very secret")
err = UpdateSecret(t.Context(), secret)
require.NoError(t, err)
updatedSecret := unittest.AssertExistsAndLoadBean(t, &Secret{ID: secret.ID})
decryptedData, err := updatedSecret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, "NEW_NAME", updatedSecret.Name)
assert.Equal(t, "also very secret", decryptedData)
}
func TestSecretUpdateSecret_RejectsInvalidName(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
secret, err := InsertEncryptedSecret(t.Context(), 2, 0, "a_secret", "very secret")
require.NoError(t, err)
secret.Name = "GITHUB_IS_REJECTED" // Because it starts with `GITHUB_`.
secret.SetData("also very secret")
err = UpdateSecret(t.Context(), secret)
require.ErrorContains(t, err, "invalid secret name")
updatedSecret := unittest.AssertExistsAndLoadBean(t, &Secret{ID: secret.ID})
decryptedData, err := updatedSecret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, "A_SECRET", updatedSecret.Name)
assert.Equal(t, "very secret", decryptedData)
}
func TestSecretValidateName(t *testing.T) {
testCases := []struct {
name string
valid bool
}{
{"FORGEJO_", false},
{"FORGEJO_123", false},
{"FORGEJO_ABC", false},
{"GITEA_", false},
{"GITEA_123", false},
{"GITEA_ABC", false},
{"GITHUB_", false},
{"GITHUB_123", false},
{"GITHUB_ABC", false},
{"123_TEST", false},
{"CI", true},
{"_CI", true},
{"CI_", true},
{"CI123", true},
{"CIABC", true},
{"FORGEJO", true},
{"FORGEJO123", true},
{"FORGEJOABC", true},
{"GITEA", true},
{"GITEA123", true},
{"GITEAABC", true},
{"GITHUB", true},
{"GITHUB123", true},
{"GITHUBABC", true},
{"_123_TEST", true},
}
for _, tC := range testCases {
t.Run(tC.name, func(t *testing.T) {
t.Helper()
if tC.valid {
assert.NoError(t, ValidateName(tC.name))
assert.NoError(t, ValidateName(strings.ToLower(tC.name)))
} else {
require.ErrorIs(t, ValidateName(tC.name), ErrInvalidName)
require.ErrorIs(t, ValidateName(strings.ToLower(tC.name)), ErrInvalidName)
}
})
}
}

View file

@ -534,6 +534,12 @@
"actions.secrets.creation.value_description": "The value of a secret can be any text. Special characters are retained. CRLF (Windows-style line breaks) is automatically converted to LF. Encode the value with Base64 if linebreaks should be retained.",
"actions.variables.mutation.name_description": "The name of a variable can only contain letters, numbers, and underscores. It cannot be named CI or start with FORGEJO_, GITEA_, GITHUB_, or a number. Forgejo will automatically convert it to uppercase.",
"actions.variables.mutation.value_description": "A variable's value can be any text. Special characters are retained. CRLF (Windows-style line breaks) is automatically converted to LF. Encode the value with Base64 if linebreaks should be retained.",
"actions.secrets.edit_button": "Edit secret \"%s\"",
"actions.secrets.mutation.header": "Edit secret \"%s\"",
"actions.secrets.mutation.success_message": "The secret \"%s\" has been updated.",
"actions.secrets.mutation.failure_message": "The secret \"%s\" could not be updated.",
"actions.secrets.mutation.name_description": "The name of a secret can only contain letters, numbers, and underscores. It cannot start with FORGEJO_, GITEA_, GITHUB_, or a number. Forgejo will automatically convert it to uppercase.",
"actions.secrets.mutation.value_description": "The existing value will not be shown. Leave the field empty if you do not want to modify it. Special characters are retained. CRLF (Windows-style line breaks) is automatically converted to LF. Encode the value with Base64 if linebreaks should be retained.",
"pulse.n_active_issues": {
"one": "%s active issue",
"other": "%s active issues"

View file

@ -92,7 +92,7 @@ func Secrets(ctx *context.Context) {
ctx.HTML(http.StatusOK, sCtx.SecretsTemplate)
}
func SecretsPost(ctx *context.Context) {
func SecretsCreatePost(ctx *context.Context) {
sCtx, err := getSecretsCtx(ctx)
if err != nil {
ctx.ServerError("getSecretsCtx", err)
@ -104,7 +104,7 @@ func SecretsPost(ctx *context.Context) {
return
}
shared.PerformSecretsPost(
shared.CreateSecretPost(
ctx,
sCtx.OwnerID,
sCtx.RepoID,
@ -112,16 +112,32 @@ func SecretsPost(ctx *context.Context) {
)
}
func SecretsDelete(ctx *context.Context) {
func SecretsEditPost(ctx *context.Context) {
sCtx, err := getSecretsCtx(ctx)
if err != nil {
ctx.ServerError("getSecretsCtx", err)
return
}
shared.PerformSecretsDelete(
if ctx.HasError() {
ctx.JSONError(ctx.GetErrMsg())
return
}
shared.EditSecretPost(ctx, sCtx.OwnerID, sCtx.RepoID, ctx.ParamsInt64(":secret_id"), sCtx.RedirectLink)
}
func SecretsDeletePost(ctx *context.Context) {
sCtx, err := getSecretsCtx(ctx)
if err != nil {
ctx.ServerError("getSecretsCtx", err)
return
}
shared.DeleteSecretPost(
ctx,
sCtx.OwnerID,
sCtx.RepoID,
ctx.ParamsInt64(":secret_id"),
sCtx.RedirectLink,
)
}

View file

@ -4,6 +4,8 @@
package secrets
import (
"errors"
"forgejo.org/models/db"
secret_model "forgejo.org/models/secret"
"forgejo.org/modules/log"
@ -24,23 +26,50 @@ func SetSecretsContext(ctx *context.Context, ownerID, repoID int64) {
ctx.Data["Secrets"] = secrets
}
func PerformSecretsPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.AddSecretForm)
func CreateSecretPost(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.CreateSecretForm)
s, _, err := secrets_service.CreateOrUpdateSecret(ctx, ownerID, repoID, form.Name, util.ReserveLineBreakForTextarea(form.Data))
normalizedData := util.ReserveLineBreakForTextarea(form.Data)
secret, err := secret_model.InsertEncryptedSecret(ctx, ownerID, repoID, form.Name, normalizedData)
if err != nil {
log.Error("CreateOrUpdateSecret failed: %v", err)
log.Error("InsertEncryptedSecret failed: %v", err)
ctx.JSONError(ctx.Tr("secrets.creation.failed"))
return
}
ctx.Flash.Success(ctx.Tr("secrets.creation.success", s.Name))
ctx.Flash.Success(ctx.Tr("secrets.creation.success", secret.Name))
ctx.JSONRedirect(redirectURL)
}
func PerformSecretsDelete(ctx *context.Context, ownerID, repoID int64, redirectURL string) {
id := ctx.FormInt64("id")
func EditSecretPost(ctx *context.Context, ownerID, repoID, id int64, redirectURL string) {
form := web.GetForm(ctx).(*forms.EditSecretForm)
secret, err := secret_model.GetSecretByID(ctx, ownerID, repoID, id)
if errors.Is(err, util.ErrNotExist) {
ctx.NotFound("GetSecretByID", err)
return
} else if err != nil {
ctx.ServerError("GetSecretByID", err)
return
}
secret.Name = form.Name
if form.Data != "" {
secret.SetData(util.ReserveLineBreakForTextarea(form.Data))
}
err = secret_model.UpdateSecret(ctx, secret)
if err != nil {
log.Error("UpdateSecret failed: %v", err)
ctx.JSONError(ctx.Tr("actions.secrets.mutation.failure_message", secret.Name))
return
}
ctx.Flash.Success(ctx.Tr("actions.secrets.mutation.success_message", secret.Name))
ctx.JSONRedirect(redirectURL)
}
func DeleteSecretPost(ctx *context.Context, ownerID, repoID, id int64, redirectURL string) {
err := secrets_service.DeleteSecretByID(ctx, ownerID, repoID, id)
if err != nil {
log.Error("DeleteSecretByID(%d) failed: %v", id, err)

View file

@ -454,8 +454,9 @@ func registerRoutes(m *web.Route) {
addSettingsSecretsRoutes := func() {
m.Group("/secrets", func() {
m.Get("", repo_setting.Secrets)
m.Post("", web.Bind(forms.AddSecretForm{}), repo_setting.SecretsPost)
m.Post("/delete", repo_setting.SecretsDelete)
m.Post("", web.Bind(forms.CreateSecretForm{}), repo_setting.SecretsCreatePost)
m.Post("/{secret_id}/edit", web.Bind(forms.EditSecretForm{}), repo_setting.SecretsEditPost)
m.Post("/{secret_id}/delete", repo_setting.SecretsDeletePost)
})
}

View file

@ -9,13 +9,13 @@ import (
"strings"
actions_model "forgejo.org/models/actions"
"forgejo.org/models/secret"
"forgejo.org/modules/log"
"forgejo.org/modules/util"
secrets_service "forgejo.org/services/secrets"
)
func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data string) (*actions_model.ActionVariable, error) {
if err := secrets_service.ValidateName(name); err != nil {
if err := secret.ValidateName(name); err != nil {
return nil, err
}
@ -32,7 +32,7 @@ func CreateVariable(ctx context.Context, ownerID, repoID int64, name, data strin
}
func UpdateVariable(ctx context.Context, variableID, ownerID, repoID int64, name, data string) (bool, error) {
if err := secrets_service.ValidateName(name); err != nil {
if err := secret.ValidateName(name); err != nil {
return false, err
}

38
services/forms/secret.go Normal file
View file

@ -0,0 +1,38 @@
// Copyright 2014 The Gogs Authors. All rights reserved.
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package forms
import (
"net/http"
"forgejo.org/modules/web/middleware"
"forgejo.org/services/context"
"code.forgejo.org/go-chi/binding"
)
// CreateSecretForm needs to be filled in by the user to create a new secret.
type CreateSecretForm struct {
Name string `binding:"Required;MaxSize(255)"`
Data string `binding:"Required;MaxSize(65535)"`
}
// Validate validates the submitted CreateSecretForm.
func (f *CreateSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// EditSecretForm needs to be filled in by the user to change an existing secret.
type EditSecretForm struct {
Name string `binding:"Required;MaxSize(255)"`
Data string `binding:"MaxSize(65535)"`
}
// Validate validates the submitted EditSecretForm.
func (f *EditSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

View file

@ -346,18 +346,6 @@ func (f *AddKeyForm) Validate(req *http.Request, errs binding.Errors) binding.Er
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
// AddSecretForm for adding secrets
type AddSecretForm struct {
Name string `binding:"Required;MaxSize(255)"`
Data string `binding:"Required;MaxSize(65535)"`
}
// Validate validates the fields
func (f *AddSecretForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}
type EditVariableForm struct {
Name string `binding:"Required;MaxSize(255)"`
Data string `binding:"Required;MaxSize(65535)"`

View file

@ -11,7 +11,7 @@ import (
)
func CreateOrUpdateSecret(ctx context.Context, ownerID, repoID int64, name, data string) (*secret_model.Secret, bool, error) {
if err := ValidateName(name); err != nil {
if err := secret_model.ValidateName(name); err != nil {
return nil, false, err
}

View file

@ -1,25 +0,0 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package secrets
import (
"regexp"
"forgejo.org/modules/util"
)
// https://docs.github.com/en/actions/security-guides/encrypted-secrets#naming-your-secrets
var (
namePattern = regexp.MustCompile("(?i)^[A-Z_][A-Z0-9_]*$")
forbiddenPrefixPattern = regexp.MustCompile("(?i)^FORGEJO_|GITEA_|GITHUB_")
ErrInvalidName = util.NewInvalidArgumentErrorf("invalid secret name")
)
func ValidateName(name string) error {
if !namePattern.MatchString(name) || forbiddenPrefixPattern.MatchString(name) {
return ErrInvalidName
}
return nil
}

View file

@ -1,57 +0,0 @@
// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package secrets
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestValidateName(t *testing.T) {
testCases := []struct {
name string
valid bool
}{
{"FORGEJO_", false},
{"FORGEJO_123", false},
{"FORGEJO_ABC", false},
{"GITEA_", false},
{"GITEA_123", false},
{"GITEA_ABC", false},
{"GITHUB_", false},
{"GITHUB_123", false},
{"GITHUB_ABC", false},
{"123_TEST", false},
{"CI", true},
{"_CI", true},
{"CI_", true},
{"CI123", true},
{"CIABC", true},
{"FORGEJO", true},
{"FORGEJO123", true},
{"FORGEJOABC", true},
{"GITEA", true},
{"GITEA123", true},
{"GITEAABC", true},
{"GITHUB", true},
{"GITHUB123", true},
{"GITHUBABC", true},
{"_123_TEST", true},
}
for _, tC := range testCases {
t.Run(tC.name, func(t *testing.T) {
t.Helper()
if tC.valid {
assert.NoError(t, ValidateName(tC.name))
assert.NoError(t, ValidateName(strings.ToLower(tC.name)))
} else {
require.ErrorIs(t, ValidateName(tC.name), ErrInvalidName)
require.ErrorIs(t, ValidateName(strings.ToLower(tC.name)), ErrInvalidName)
}
})
}
}

View file

@ -30,8 +30,18 @@
<span class="color-text-light-2">
{{ctx.Locale.Tr "settings.added_on" (DateUtils.AbsoluteShort .CreatedUnix)}}
</span>
<button class="btn interact-bg tw-p-2 show-modal"
data-tooltip-content="{{ctx.Locale.Tr "actions.secrets.edit_button" .Name}}"
data-modal="#edit-secret-modal"
data-modal-form.action="{{$.Link}}/{{.ID}}/edit"
data-modal-header="{{ctx.Locale.Tr "actions.secrets.mutation.header" .Name}}"
data-modal-dialog-secret-name="{{.Name}}"
data-modal-dialog-secret-data=""
>
{{svg "octicon-pencil"}}
</button>
<button class="ui btn interact-bg link-action tw-p-2"
data-url="{{$.Link}}/delete?id={{.ID}}"
data-url="{{$.Link}}/{{.ID}}/delete"
data-modal-confirm="{{ctx.Locale.Tr "secrets.deletion.description"}}"
data-tooltip-content="{{ctx.Locale.Tr "secrets.deletion"}}"
>
@ -57,7 +67,7 @@
{{ctx.Locale.Tr "secrets.description"}}
</div>
<div class="field">
<label for="secret-name">{{ctx.Locale.Tr "name"}}</label>
<label class="required" for="secret-name">{{ctx.Locale.Tr "name"}}</label>
<input autofocus required
id="secret-name"
name="name"
@ -67,7 +77,7 @@
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.secrets.creation.name_description"}}</p>
</div>
<div class="field">
<label for="secret-data">{{ctx.Locale.Tr "value"}}</label>
<label class="required" for="secret-data">{{ctx.Locale.Tr "value"}}</label>
<textarea required
id="secret-data"
name="data"
@ -78,3 +88,32 @@
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
{{/* Edit secret dialog */}}
<div class="ui small modal" id="edit-secret-modal">
<div class="header">
<span id="actions-modal-header"></span>
</div>
<form class="ui form form-fetch-action" method="post">
<fieldset class="content">
<div class="field">
{{ctx.Locale.Tr "secrets.description"}}
</div>
<div class="field">
<label class="required" for="dialog-secret-name">{{ctx.Locale.Tr "name"}}</label>
<input autofocus required
id="dialog-secret-name"
name="name"
value="{{.name}}"
pattern="^(?!FORGEJO_|GITEA_|GITHUB_)[a-zA-Z_][a-zA-Z0-9_]*$"
>
<p id="name-description" class="help">{{ctx.Locale.Tr "actions.secrets.mutation.name_description"}}</p>
</div>
<div class="field">
<label for="dialog-secret-data">{{ctx.Locale.Tr "value"}}</label>
<textarea id="dialog-secret-data" name="data"></textarea>
<p id="secret-data-description" class="help">{{ctx.Locale.Tr "actions.secrets.mutation.value_description"}}</p>
</div>
</fieldset>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>

View file

@ -19,156 +19,388 @@ import (
"github.com/stretchr/testify/require"
)
func TestActionsSecretsManageUserSecrets(t *testing.T) {
func TestActionsSecretsCreateSecret(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
url := "/user/settings/actions/secrets"
session := loginUser(t, user.Name)
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
t.Run("Create secret", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", url, map[string]string{
"name": "my_secret",
"data": " \r\n\tSecrët dåtä\\ \r\n",
sess := loginUser(t, user2.Name)
testCases := []struct {
name string
url string
ownerID int64
repoID int64
}{
{
name: "User",
url: "/user/settings/actions/secrets",
ownerID: user2.ID,
repoID: 0,
},
{
name: "Repository",
url: "/" + repo1.FullName() + "/settings/actions/secrets",
ownerID: 0,
repoID: repo1.ID,
},
{
name: "Organization",
url: "/org/" + org3.Name + "/settings/actions/secrets",
ownerID: org3.ID,
repoID: 0,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
"name": "my_secret",
"data": " \r\n\tSecrët dåtä\\ \r\n",
})
sess.MakeRequest(t, req, http.StatusOK)
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522MY_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "MY_SECRET"})
assert.Equal(t, "MY_SECRET", secret.Name)
value, err := secret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, " \n\tSecrët dåtä\\ \n", value)
})
session.MakeRequest(t, req, http.StatusOK)
flashCookie := session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522MY_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: user.ID, RepoID: 0, Name: "MY_SECRET"})
assert.Equal(t, "MY_SECRET", secret.Name)
value, err := secret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, " \n\tSecrët dåtä\\ \n", value)
})
t.Run("Remove secret", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", url, map[string]string{
"name": "TEST_SECRET",
"data": "value",
})
session.MakeRequest(t, req, http.StatusOK)
flashCookie := session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: user.ID, RepoID: 0, Name: "TEST_SECRET"})
req = NewRequest(t, "POST", fmt.Sprintf("%s/delete?id=%d", url, secret.ID))
session.MakeRequest(t, req, http.StatusOK)
flashCookie = session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
unittest.AssertNotExistsBean(t, secret)
})
}
}
func TestActionsSecretsManageRepositorySecrets(t *testing.T) {
func TestActionsSecretsCreateSecretRejectsNameMatchingExistingSecret(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user.ID})
url := "/" + repo.FullName() + "/settings/actions/secrets"
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
session := loginUser(t, user.Name)
sess := loginUser(t, user2.Name)
t.Run("Create secret", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", url, map[string]string{
"name": "my_secret",
"data": " \r\n\tSecrët dåtä\\ \r\n",
testCases := []struct {
name string
url string
ownerID int64
repoID int64
}{
{
name: "User",
url: "/user/settings/actions/secrets",
ownerID: user2.ID,
repoID: 0,
},
{
name: "Repository",
url: "/" + repo1.FullName() + "/settings/actions/secrets",
ownerID: 0,
repoID: repo1.ID,
},
{
name: "Organization",
url: "/org/" + org3.Name + "/settings/actions/secrets",
ownerID: org3.ID,
repoID: 0,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
"name": "my_secret",
"data": "original value",
})
sess.MakeRequest(t, req, http.StatusOK)
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522MY_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "MY_SECRET"})
assert.Equal(t, "MY_SECRET", secret.Name)
value, err := secret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, "original value", value)
// Try to create a new secret with the name but another value.
req = NewRequestWithValues(t, "POST", testCase.url, map[string]string{
"name": "my_secret",
"data": "changed value",
})
sess.MakeRequest(t, req, http.StatusBadRequest)
// Verify that the original secret has not been changed.
secret = unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "MY_SECRET"})
value, err = secret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, "MY_SECRET", secret.Name)
assert.Equal(t, "original value", value)
})
session.MakeRequest(t, req, http.StatusOK)
flashCookie := session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522MY_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: 0, RepoID: repo.ID, Name: "MY_SECRET"})
assert.Equal(t, "MY_SECRET", secret.Name)
value, err := secret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, " \n\tSecrët dåtä\\ \n", value)
})
t.Run("Remove secret", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", url, map[string]string{
"name": "TEST_SECRET",
"data": "value",
})
session.MakeRequest(t, req, http.StatusOK)
flashCookie := session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: 0, RepoID: repo.ID, Name: "TEST_SECRET"})
req = NewRequest(t, "POST", fmt.Sprintf("%s/delete?id=%d", url, secret.ID))
session.MakeRequest(t, req, http.StatusOK)
flashCookie = session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
unittest.AssertNotExistsBean(t, secret)
})
}
}
func TestActionsSecretsManageOrganizationSecrets(t *testing.T) {
func TestActionsSecretsEditSecret(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
url := "/org/" + org.Name + "/settings/actions/secrets"
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
session := loginUser(t, user.Name)
sess := loginUser(t, user2.Name)
t.Run("Create secret", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", url, map[string]string{
"name": "my_secret",
"data": " \r\n\tSecrët dåtä\\ \r\n",
testCases := []struct {
name string
url string
ownerID int64
repoID int64
}{
{
name: "User",
url: "/user/settings/actions/secrets",
ownerID: user2.ID,
repoID: 0,
},
{
name: "Repository",
url: "/" + repo1.FullName() + "/settings/actions/secrets",
ownerID: 0,
repoID: repo1.ID,
},
{
name: "Organization",
url: "/org/" + org3.Name + "/settings/actions/secrets",
ownerID: org3.ID,
repoID: 0,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
"name": "TEST_SECRET",
"data": "value",
})
sess.MakeRequest(t, req, http.StatusOK)
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "TEST_SECRET"})
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", testCase.url, secret.ID), map[string]string{
"name": secret.Name,
"data": " \r\n\tSecrët dåtä\\ \r\n",
})
sess.MakeRequest(t, req, http.StatusOK)
flashCookie = sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
updatedSecret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{ID: secret.ID})
decryptedValue, err := updatedSecret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, secret.Name, updatedSecret.Name)
assert.Equal(t, " \n\tSecrët dåtä\\ \n", decryptedValue)
})
session.MakeRequest(t, req, http.StatusOK)
flashCookie := session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522MY_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: org.ID, RepoID: 0, Name: "MY_SECRET"})
assert.Equal(t, "MY_SECRET", secret.Name)
value, err := secret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, " \n\tSecrët dåtä\\ \n", value)
})
t.Run("Remove secret", func(t *testing.T) {
req := NewRequestWithValues(t, "POST", url, map[string]string{
"name": "TEST_SECRET",
"data": "value",
})
session.MakeRequest(t, req, http.StatusOK)
flashCookie := session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: org.ID, RepoID: 0, Name: "TEST_SECRET"})
req = NewRequest(t, "POST", fmt.Sprintf("%s/delete?id=%d", url, secret.ID))
session.MakeRequest(t, req, http.StatusOK)
flashCookie = session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
unittest.AssertNotExistsBean(t, secret)
})
}
}
func TestActionsSecretsEditSecretWithoutChangingItsValue(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
sess := loginUser(t, user2.Name)
testCases := []struct {
name string
url string
ownerID int64
repoID int64
}{
{
name: "User",
url: "/user/settings/actions/secrets",
ownerID: user2.ID,
repoID: 0,
},
{
name: "Repository",
url: "/" + repo1.FullName() + "/settings/actions/secrets",
ownerID: 0,
repoID: repo1.ID,
},
{
name: "Organization",
url: "/org/" + org3.Name + "/settings/actions/secrets",
ownerID: org3.ID,
repoID: 0,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
"name": "TEST_SECRET",
"data": " \r\n\tSecrët dåtä\\ \r\n",
})
sess.MakeRequest(t, req, http.StatusOK)
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "TEST_SECRET"})
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", testCase.url, secret.ID), map[string]string{
"name": "changed_secret",
"data": "",
})
sess.MakeRequest(t, req, http.StatusOK)
flashCookie = sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522CHANGED_SECRET%2522%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
updatedSecret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{ID: secret.ID})
decryptedValue, err := updatedSecret.GetDecryptedData()
require.NoError(t, err)
assert.Equal(t, "CHANGED_SECRET", updatedSecret.Name)
assert.Equal(t, " \n\tSecrët dåtä\\ \n", decryptedValue)
})
}
}
func TestActionsSecretsEditSecretRejectsInvalidName(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
sess := loginUser(t, user2.Name)
testCases := []struct {
name string
url string
ownerID int64
repoID int64
}{
{
name: "User",
url: "/user/settings/actions/secrets",
ownerID: user2.ID,
repoID: 0,
},
{
name: "Repository",
url: "/" + repo1.FullName() + "/settings/actions/secrets",
ownerID: 0,
repoID: repo1.ID,
},
{
name: "Organization",
url: "/org/" + org3.Name + "/settings/actions/secrets",
ownerID: org3.ID,
repoID: 0,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
"name": "TEST_SECRET",
"data": "value",
})
sess.MakeRequest(t, req, http.StatusOK)
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "TEST_SECRET"})
req = NewRequestWithValues(t, "POST", fmt.Sprintf("%s/%d/edit", testCase.url, secret.ID), map[string]string{
"name": "FORGEJO_IS_INVALID",
"data": "",
})
sess.MakeRequest(t, req, http.StatusBadRequest)
})
}
}
func TestActionsSecretsRemoveSecret(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, OwnerID: user2.ID})
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3, Type: user_model.UserTypeOrganization})
sess := loginUser(t, user2.Name)
testCases := []struct {
name string
url string
ownerID int64
repoID int64
}{
{
name: "User",
url: "/user/settings/actions/secrets",
ownerID: user2.ID,
repoID: 0,
},
{
name: "Repository",
url: "/" + repo1.FullName() + "/settings/actions/secrets",
ownerID: 0,
repoID: repo1.ID,
},
{
name: "Organization",
url: "/org/" + org3.Name + "/settings/actions/secrets",
ownerID: org3.ID,
repoID: 0,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
req := NewRequestWithValues(t, "POST", testCase.url, map[string]string{
"name": "TEST_SECRET",
"data": "value",
})
sess.MakeRequest(t, req, http.StatusOK)
flashCookie := sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2B%2522TEST_SECRET%2522%2Bhas%2Bbeen%2Badded.", flashCookie.Value)
secret := unittest.AssertExistsAndLoadBean(t, &secret_model.Secret{OwnerID: testCase.ownerID, RepoID: testCase.repoID, Name: "TEST_SECRET"})
req = NewRequest(t, "POST", fmt.Sprintf("%s/%d/delete", testCase.url, secret.ID))
sess.MakeRequest(t, req, http.StatusOK)
flashCookie = sess.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "success%3DThe%2Bsecret%2Bhas%2Bbeen%2Bremoved.", flashCookie.Value)
unittest.AssertNotExistsBean(t, secret)
})
}
}

View file

@ -10,6 +10,7 @@ import (
"testing"
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
org_model "forgejo.org/models/organization"
secret_model "forgejo.org/models/secret"
"forgejo.org/models/unittest"
@ -157,7 +158,8 @@ func TestAPIOrgSecrets(t *testing.T) {
})
t.Run("Delete with forbidden names", func(t *testing.T) {
secret, err := secret_model.InsertEncryptedSecret(t.Context(), org.ID, 0, "FORGEJO_FORBIDDEN", "illegal")
secret := secret_model.Secret{OwnerID: org.ID, RepoID: 0, Name: "FORGEJO_FORBIDDEN"}
err := db.Insert(t.Context(), secret)
require.NoError(t, err)
url := fmt.Sprintf("/api/v1/orgs/%s/actions/secrets/%s", org.Name, secret.Name)

View file

@ -158,7 +158,8 @@ func TestAPIRepoSecrets(t *testing.T) {
})
t.Run("Delete with forbidden names", func(t *testing.T) {
secret, err := secret_model.InsertEncryptedSecret(t.Context(), 0, repo.ID, "FORGEJO_FORBIDDEN", "illegal")
secret := secret_model.Secret{OwnerID: 0, RepoID: repo.ID, Name: "FORGEJO_FORBIDDEN"}
err := db.Insert(t.Context(), secret)
require.NoError(t, err)
url := fmt.Sprintf("/api/v1/repos/%s/actions/secrets/%s", repo.FullName(), secret.Name)

View file

@ -10,6 +10,7 @@ import (
"testing"
auth_model "forgejo.org/models/auth"
"forgejo.org/models/db"
secret_model "forgejo.org/models/secret"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
@ -122,7 +123,8 @@ func TestAPIUserSecrets(t *testing.T) {
})
t.Run("Delete with forbidden names", func(t *testing.T) {
secret, err := secret_model.InsertEncryptedSecret(t.Context(), user.ID, 0, "FORGEJO_FORBIDDEN", "illegal")
secret := secret_model.Secret{OwnerID: user.ID, RepoID: 0, Name: "FORGEJO_FORBIDDEN"}
err := db.Insert(t.Context(), secret)
require.NoError(t, err)
url := fmt.Sprintf("/api/v1/user/actions/secrets/%s", secret.Name)