diff --git a/modules/test/utils.go b/modules/test/utils.go index db131f19d0..af22872f44 100644 --- a/modules/test/utils.go +++ b/modules/test/utils.go @@ -52,3 +52,9 @@ func MockProtect[T any](p *T) (reset func()) { func SleepTillNextSecond() { time.Sleep(time.Second - time.Since(time.Now().Truncate(time.Second))) } + +// When this is called, sleep until the truncated unix time to a minute was +// increased by one. +func SleepTillNextMinute() { + time.Sleep(time.Minute - time.Since(time.Now().Truncate(time.Minute))) +} diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json index 77aa288f48..7d559f0414 100644 --- a/options/locale_next/locale_en-US.json +++ b/options/locale_next/locale_en-US.json @@ -109,6 +109,7 @@ "feed.atom.link": "Atom feed", "keys.ssh.link": "SSH keys", "keys.gpg.link": "GPG keys", + "keys.verify.token.hint": "The token is only valid for 1 minute. Get a new one if it expired.", "admin.config.moderation_config": "Moderation configuration", "admin.moderation.moderation_reports": "Moderation reports", "admin.moderation.reports": "Reports", diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl index c031177a00..628c1edc95 100644 --- a/templates/user/settings/keys_gpg.tmpl +++ b/templates/user/settings/keys_gpg.tmpl @@ -19,6 +19,7 @@
{{ctx.Locale.Tr "settings.gpg_token_help"}}
{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` .TokenToSign .PaddedKeyID}}
{{ctx.Locale.Tr "settings.gpg_token_help"}}
{{printf `echo "%s" | gpg -a --default-key %s --detach-sig` $.TokenToSign .PaddedKeyID}}
{{ctx.Locale.Tr "settings.ssh_token_help"}}
diff --git a/tests/integration/user_keys_test.go b/tests/integration/user_keys_test.go new file mode 100644 index 0000000000..5992c1e111 --- /dev/null +++ b/tests/integration/user_keys_test.go @@ -0,0 +1,51 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "strings" + "testing" + + "forgejo.org/modules/test" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestVerifySSHkeyPage(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // user2 has an SSH key in fixtures to test this on + session := loginUser(t, "user2") + + page := NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", "/user/settings/keys"), http.StatusOK).Body) + link, exists := page.Find("#keys-ssh a.button[href^='?verify_ssh=']").Attr("href") + assert.True(t, exists) + + page = NewHTMLParser(t, session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("/user/settings/keys%s", link)), http.StatusOK).Body) + + // QueryUnescape the link for selector matching + link, err := url.QueryUnescape(link) + require.NoError(t, err) + + // The hint contains a link to the same page the user is at now to get it reloaded if followed + page.AssertElement(t, fmt.Sprintf("#keys-ssh form[action='/user/settings/keys'] .help a[href='%s']", link), true) + + // The token changes every minute, we can avoid this sleep via timeutil and mocking. + test.SleepTillNextMinute() + + // Verify that if you refresh it via the link another token is shown. + token, exists := page.Find("#keys-ssh form input[readonly]").Attr("value") + assert.True(t, exists) + + link = url.QueryEscape(strings.TrimPrefix(link, "?verify_ssh=")) + page = NewHTMLParser(t, session.MakeRequest(t, NewRequestf(t, "GET", "/user/settings/keys?verify_ssh=%s", link), http.StatusOK).Body) + otherToken, exists := page.Find("#keys-ssh form .field input[readonly]").Attr("value") + assert.True(t, exists) + assert.NotEqual(t, token, otherToken) +}