jojo/tests/integration/mirror_pull_test.go
Mathieu Fenniak 6a99b6b0c1 fix: store pull mirror creds encrypted with keying (#11909)
Fixes #9629.

New pull mirrors have credentials stored encrypted in the database, the same as push mirrors, rather than in the repository's `config` file.  `git fetch` on the pull mirror is updated to use the credential store.  Pull mirrors will have their credentials migrated to the encrypted storage in the database as they're synced or otherwise accessed via the web UI.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). 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

- I added test coverage for Go changes...
  - [ ] 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

### 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.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11909
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
2026-04-04 13:53:22 +02:00

579 lines
20 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"fmt"
"net/http"
"net/url"
"os"
"path"
"strings"
"testing"
"time"
"forgejo.org/models/auth"
"forgejo.org/models/db"
repo_model "forgejo.org/models/repo"
"forgejo.org/models/unittest"
user_model "forgejo.org/models/user"
"forgejo.org/modules/git"
"forgejo.org/modules/gitrepo"
"forgejo.org/modules/migration"
"forgejo.org/modules/optional"
"forgejo.org/modules/process"
"forgejo.org/modules/setting"
"forgejo.org/modules/structs"
app_context "forgejo.org/services/context"
"forgejo.org/services/forms"
"forgejo.org/services/migrations"
mirror_service "forgejo.org/services/mirror"
release_service "forgejo.org/services/release"
repo_service "forgejo.org/services/repository"
files_service "forgejo.org/services/repository/files"
"forgejo.org/tests"
"github.com/PuerkitoBio/goquery"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMirrorPull(t *testing.T) {
t.Run("Basic", func(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})
repoPath := repo_model.RepoPath(user.Name, repo.Name)
opts := migration.MigrateOptions{
RepoName: "test_mirror",
Description: "Test mirror",
Private: false,
Mirror: true,
CloneAddr: repoPath,
Wiki: true,
Releases: false,
}
mirrorRepo, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, user, repo_service.CreateRepoOptions{
Name: opts.RepoName,
Description: opts.Description,
IsPrivate: opts.Private,
IsMirror: opts.Mirror,
Status: repo_model.RepositoryBeingMigrated,
})
require.NoError(t, err)
assert.True(t, mirrorRepo.IsMirror, "expected pull-mirror repo to be marked as a mirror immediately after its creation")
ctx := t.Context()
mirror, err := repo_service.MigrateRepositoryGitData(ctx, user, mirrorRepo, opts, nil)
require.NoError(t, err)
gitRepo, err := gitrepo.OpenRepository(git.DefaultContext, repo)
require.NoError(t, err)
defer gitRepo.Close()
findOptions := repo_model.FindReleasesOptions{
IncludeDrafts: true,
IncludeTags: true,
RepoID: mirror.ID,
}
initCount, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
require.NoError(t, err)
require.NoError(t, release_service.CreateRelease(gitRepo, &repo_model.Release{
RepoID: repo.ID,
Repo: repo,
PublisherID: user.ID,
Publisher: user,
TagName: "v0.2",
Target: "master",
Title: "v0.2 is released",
Note: "v0.2 is released",
IsDraft: false,
IsPrerelease: false,
IsTag: true,
}, "", []*release_service.AttachmentChange{}))
_, err = repo_model.GetMirrorByRepoID(ctx, mirror.ID)
require.NoError(t, err)
ok := mirror_service.SyncPullMirror(ctx, mirror.ID)
assert.True(t, ok)
count, err := db.Count[repo_model.Release](db.DefaultContext, findOptions)
require.NoError(t, err)
assert.Equal(t, initCount+1, count)
release, err := repo_model.GetRelease(db.DefaultContext, repo.ID, "v0.2")
require.NoError(t, err)
require.NoError(t, release_service.DeleteReleaseByID(ctx, repo, release, user, true))
ok = mirror_service.SyncPullMirror(ctx, mirror.ID)
assert.True(t, ok)
count, err = db.Count[repo_model.Release](db.DefaultContext, findOptions)
require.NoError(t, err)
assert.Equal(t, initCount, count)
})
// How will we interact with the pull mirror during this test?
interactionMethod := []struct {
name string
useAPI bool
createPullMirror func(t *testing.T, sourceRepo *repo_model.Repository, authenticate bool) (repoName string)
verifyMirrorDetails func(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool)
triggerPullMirror func(t *testing.T, mirrorName string)
changePullMirrorCredentials func(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool)
changePullMirrorAddress func(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool)
}{
{
name: "API",
useAPI: true,
createPullMirror: createPullMirrorViaAPI,
triggerPullMirror: triggerPullMirrorViaAPI,
verifyMirrorDetails: func(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool) {
// API provides no visibility into a repo's mirror settings right now
},
},
{
name: "Web",
useAPI: false,
createPullMirror: createPullMirrorViaWeb,
triggerPullMirror: triggerPullMirrorViaWeb,
verifyMirrorDetails: verifyPullMirrorViaWeb,
changePullMirrorCredentials: changePullMirrorCredentialsViaWeb,
changePullMirrorAddress: changePullMirrorCredentialsViaWeb, // one endpoint, so same as creds
},
}
mirrorConfiguration := []struct {
name string
privateSource bool
}{
{
name: "HTTP Without Auth",
},
{
name: "HTTP With Auth",
privateSource: true,
},
}
// Not using MockVariableValue due to need to undo `migrations.Init()`
prev := setting.Migrations.AllowedDomains
setting.Migrations.AllowedDomains = "localhost"
migrations.Init() // reinitialize for changed allowList
defer func() {
setting.Migrations.AllowedDomains = prev
migrations.Init() // reinitialize for changed allowList
}()
onApplicationRun(t, func(t *testing.T, u *url.URL) {
for _, im := range interactionMethod {
for _, mc := range mirrorConfiguration {
t.Run(fmt.Sprintf("%s/%s", im.name, mc.name), func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
// Create the source repository that will be mirrored.
sourceRepo, sourceRepoSha, cleanupSource := tests.CreateDeclarativeRepoWithOptions(t, user2,
tests.DeclarativeRepoOptions{
IsPrivate: optional.Some(mc.privateSource),
Files: optional.Some([]*files_service.ChangeRepoFile{
{
Operation: "create",
TreePath: "docs.md",
ContentReader: strings.NewReader("hello, world"),
},
}),
},
)
defer cleanupSource()
require.NotEmpty(t, sourceRepoSha)
// Create pull mirror
mirror := im.createPullMirror(t, sourceRepo, mc.privateSource)
verifyPullMirrorContents(t, mirror, sourceRepoSha)
verifyPullMirrorConfig(t, mirror, sourceRepo, mc.privateSource)
im.verifyMirrorDetails(t, sourceRepo, mirror, mc.privateSource)
// Push a change to the source and refresh the mirror
sourceRepoSha = changePullMirrorSource(t, sourceRepo, sourceRepoSha)
im.triggerPullMirror(t, mirror)
waitForPullMirror(t, mirror, sourceRepoSha)
// Test changing the mirror's authentication method (if available)
if mc.privateSource && im.changePullMirrorCredentials != nil {
sourceRepoSha = changePullMirrorSource(t, sourceRepo, sourceRepoSha)
im.changePullMirrorCredentials(t, sourceRepo, mirror, mc.privateSource)
verifyPullMirrorConfig(t, mirror, sourceRepo, mc.privateSource)
im.verifyMirrorDetails(t, sourceRepo, mirror, mc.privateSource)
im.triggerPullMirror(t, mirror)
waitForPullMirror(t, mirror, sourceRepoSha)
}
// Test changing the mirror's address (if available)
if im.changePullMirrorAddress != nil {
sourceRepo = renamePullMirrorSourceRepo(t, sourceRepo)
sourceRepoSha = changePullMirrorSource(t, sourceRepo, sourceRepoSha)
im.changePullMirrorAddress(t, sourceRepo, mirror, mc.privateSource)
verifyPullMirrorConfig(t, mirror, sourceRepo, mc.privateSource)
im.verifyMirrorDetails(t, sourceRepo, mirror, mc.privateSource)
im.triggerPullMirror(t, mirror)
waitForPullMirror(t, mirror, sourceRepoSha)
}
})
}
}
})
t.Run("migrate from repo config credentials", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
mirrorRepo, _, cleanupMirror := tests.CreateDeclarativeRepoWithOptions(t, user2,
tests.DeclarativeRepoOptions{},
)
defer cleanupMirror()
// Write to the repo a config file that would have plausibly existed before EncryptedRemoteAddress was
// introduced:
repoPath := mirrorRepo.RepoPath()
err := os.WriteFile(path.Join(repoPath, "config"), []byte(`
[core]
repositoryformatversion = 0
filemode = true
bare = true
[remote "origin"]
url = https://user:password@example.com/org/repo.git
tagOpt = --no-tags
fetch = +refs/*:refs/*
mirror = true
fetch = +refs/tags/*:refs/tags/*
`), 0o644)
require.NoError(t, err)
// Create a Mirror record without an EncryptedRemoteAddress:
mirror := &repo_model.Mirror{
RepoID: mirrorRepo.ID,
Interval: 8 * time.Hour,
EnablePrune: true,
}
_, err = db.GetEngine(t.Context()).Insert(mirror)
require.NoError(t, err)
require.Nil(t, mirror.EncryptedRemoteAddress)
remoteURL, err := mirror_service.DecryptOrRecoverRemoteAddress(t.Context(), mirror)
require.NoError(t, err)
assert.Equal(t, "https://user:password@example.com/org/repo.git", remoteURL.URL.String())
// EncryptedRemoteAddress should now be populated from the recovery:
assert.NotNil(t, mirror.EncryptedRemoteAddress)
maybeDecryptedURL, err := mirror.DecryptRemoteAddress()
require.NoError(t, err)
has, decryptedURL := maybeDecryptedURL.Get()
require.True(t, has)
assert.Equal(t, "https://user:password@example.com/org/repo.git", decryptedURL)
// SanitizedRemoteAddress can be fetched:
maybeSanitizedURL, err := mirror.SanitizedRemoteAddress()
require.NoError(t, err)
has, sanitizedURL := maybeSanitizedURL.Get()
require.True(t, has)
assert.Equal(t, "https://user@example.com/org/repo.git", sanitizedURL)
// Database record is updated in the database:
refetchMirror := unittest.AssertExistsAndLoadBean(t, &repo_model.Mirror{RepoID: mirrorRepo.ID})
assert.Equal(t, mirror.EncryptedRemoteAddress, refetchMirror.EncryptedRemoteAddress)
// Config file is rewritten:
config, err := os.ReadFile(path.Join(repoPath, "config"))
require.NoError(t, err)
assert.Equal(t, `
[core]
repositoryformatversion = 0
filemode = true
bare = true
[remote "origin"]
url = https://user@example.com/org/repo.git
tagOpt = --no-tags
fetch = +refs/*:refs/*
mirror = true
fetch = +refs/tags/*:refs/tags/*
`, string(config))
})
}
func createPullMirrorViaWeb(t *testing.T, sourceRepo *repo_model.Repository, authenticate bool) string {
session := loginUser(t, "user2")
mirrorName := fmt.Sprintf("pullmirror-%s", sourceRepo.Name)
form := &forms.MigrateRepoForm{
CloneAddr: sourceRepo.CloneLink().HTTPS,
Service: structs.PlainGitService,
UID: 2,
RepoName: mirrorName,
Mirror: true,
}
if authenticate {
form.AuthUsername = "user2"
form.AuthPassword = getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
}
resp := session.MakeRequest(t,
NewRequestWithJSON(t, "POST", "/repo/migrate", form),
http.StatusSeeOther)
location := resp.Header().Get("Location")
assert.Equal(t, fmt.Sprintf("/user2/pullmirror-%s", sourceRepo.Name), location)
var lastBody string
if !assert.Eventuallyf(t,
func() bool {
resp := session.MakeRequest(t,
NewRequest(t, "GET", location),
http.StatusOK)
body := resp.Body.String()
lastBody = body
// Looking for the repo page to be fully populated indicating that the migration is complete:
// Check that the first commit message is present:
if !strings.Contains(body, "Initial commit") {
return false
}
// Check that the fork button is present:
if !strings.Contains(body, fmt.Sprintf("/user2/%s/fork", mirrorName)) {
return false
}
return true
},
15*time.Second, 1*time.Second,
"expected migration to complete and repo page to render") {
t.Logf("last received page body: %s", lastBody)
}
return mirrorName
}
func createPullMirrorViaAPI(t *testing.T, sourceRepo *repo_model.Repository, authenticate bool) string {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeWriteRepository)
mirrorName := fmt.Sprintf("pullmirror-%s", sourceRepo.Name)
form := &structs.MigrateRepoOptions{
CloneAddr: sourceRepo.CloneLink().HTTPS,
Service: "git",
RepoOwner: "user2",
RepoName: mirrorName,
Mirror: true,
}
if authenticate {
form.AuthUsername = "user2"
form.AuthPassword = getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
}
resp := session.MakeRequest(t,
NewRequestWithJSON(t, "POST", "/api/v1/repos/migrate", form).AddTokenAuth(apiToken),
http.StatusCreated)
var repo structs.Repository
DecodeJSON(t, resp, &repo)
assert.NotNil(t, repo)
assert.True(t, repo.Mirror)
assert.False(t, repo.Empty)
return mirrorName
}
func verifyPullMirrorViaWeb(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool) {
session := loginUser(t, "user2")
resp := session.MakeRequest(t,
NewRequestf(t, "GET", "/user2/%s/settings", mirrorName),
http.StatusOK)
htmlDoc := NewHTMLParser(t, resp.Body)
htmlDoc.AssertAttrEqual(t, "#mirror_address", "value", sourceRepo.CloneLink().HTTPS)
if authenticate {
htmlDoc.AssertAttrEqual(t, "#mirror_username", "value", "user2")
htmlDoc.AssertAttrEqual(t, "#mirror_password", "value", "")
htmlDoc.AssertAttrEqual(t, "#mirror_password", "placeholder", "(Unchanged)")
} else {
htmlDoc.AssertAttrEqual(t, "#mirror_username", "value", "")
htmlDoc.AssertAttrEqual(t, "#mirror_password", "value", "")
htmlDoc.AssertAttrEqual(t, "#mirror_password", "placeholder", "(Unset)")
}
resp = session.MakeRequest(t,
NewRequestf(t, "GET", "/user2/%s", mirrorName),
http.StatusOK)
htmlDoc = NewHTMLParser(t, resp.Body)
htmlDoc.AssertElementPredicate(t, ".fork-flag", func(selection *goquery.Selection) bool {
text := strings.TrimSpace(selection.Text())
assert.Contains(t, text, "mirror of")
assert.Contains(t, text, sourceRepo.CloneLink().HTTPS)
return true
})
}
func triggerPullMirrorViaWeb(t *testing.T, mirrorName string) {
session := loginUser(t, "user2")
resp := session.MakeRequest(t,
NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/%s/settings", mirrorName), map[string]string{"action": "mirror-sync"}),
http.StatusSeeOther)
location := resp.Header().Get("Location")
assert.Equal(t, fmt.Sprintf("/user2/%s/settings", mirrorName), location)
}
func triggerPullMirrorViaAPI(t *testing.T, mirrorName string) {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeWriteRepository)
// Trigger sync...
session.MakeRequest(t,
NewRequestf(t, "POST", "/api/v1/repos/user2/%s/mirror-sync", mirrorName).AddTokenAuth(apiToken),
http.StatusOK)
}
func changePullMirrorCredentialsViaWeb(t *testing.T, sourceRepo *repo_model.Repository, mirrorName string, authenticate bool) {
session := loginUser(t, "user2")
form := map[string]string{
"action": "mirror",
"enable_prune": "on",
"interval": "8h0m0s",
"mirror_address": sourceRepo.CloneLink().HTTPS,
}
if authenticate {
form["mirror_username"] = "user2"
form["mirror_password"] = getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
}
resp := session.MakeRequest(t,
NewRequestWithValues(t, "POST", fmt.Sprintf("/user2/%s/settings", mirrorName), form),
http.StatusSeeOther)
location := resp.Header().Get("Location")
assert.Equal(t, fmt.Sprintf("/user2/%s/settings", mirrorName), location)
}
func verifyPullMirrorContents(t *testing.T, mirrorName, expectedSha string) {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
resp := session.MakeRequest(t,
NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/%s/commits?sha=main&limit=1", mirrorName)).AddTokenAuth(apiToken),
http.StatusOK)
var commits []*structs.Commit
DecodeJSON(t, resp, &commits)
require.Len(t, commits, 1)
assert.Equal(t, expectedSha, commits[0].SHA)
}
func waitForPullMirror(t *testing.T, mirrorName, expectedSha string) {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeReadRepository)
var commits []*structs.Commit
if !assert.Eventually(t,
func() bool {
resp := session.MakeRequest(t,
NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/user2/%s/commits?sha=main&limit=1", mirrorName)).AddTokenAuth(apiToken),
http.StatusOK)
DecodeJSON(t, resp, &commits)
require.Len(t, commits, 1)
return commits[0].SHA == expectedSha
},
15*time.Second, 1*time.Second) {
t.Logf("sync was supposed to bring repo to commit %s, but observed commits = %#v", expectedSha, commits)
}
}
func getGitConfig(t *testing.T, configFile, configPath string) string {
stdout, stderr, err := process.GetManager().Exec("getGitConfig", "git", "config", "get", "--file", configFile, configPath)
require.NoError(t, err, "fetch config %s failed: git stderr: %s", configPath, stderr)
return strings.TrimSpace(stdout)
}
func verifyPullMirrorConfig(t *testing.T, mirrorName string, sourceRepo *repo_model.Repository, authenticate bool) {
mirrorRepo, err := repo_model.GetRepositoryByOwnerAndName(t.Context(), "user2", mirrorName)
require.NoError(t, err)
repoPath := mirrorRepo.RepoPath()
configPath := path.Join(repoPath, "config")
expectedURL := sourceRepo.CloneLink().HTTPS
if authenticate {
expectedURL = strings.Replace(expectedURL, "http://", "http://user2@", 1)
}
assert.Equal(t, expectedURL, getGitConfig(t, configPath, "remote.origin.url"))
assert.Equal(t, "true", getGitConfig(t, configPath, "remote.origin.mirror"))
assert.Equal(t, "+refs/tags/*:refs/tags/*", getGitConfig(t, configPath, "remote.origin.fetch"))
}
func changePullMirrorSource(t *testing.T, sourceRepo *repo_model.Repository, sourceRepoSha string) string {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
resp, err := files_service.ChangeRepoFiles(git.DefaultContext, sourceRepo, user2,
&files_service.ChangeRepoFilesOptions{
Files: []*files_service.ChangeRepoFile{
{
Operation: "update",
TreePath: "docs.md",
ContentReader: strings.NewReader(uuid.NewString()),
},
},
Message: "add files",
OldBranch: "main",
NewBranch: "main",
Author: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
},
Committer: &files_service.IdentityOptions{
Name: user2.Name,
Email: user2.Email,
},
Dates: &files_service.CommitDateOptions{
Author: time.Now(),
Committer: time.Now(),
},
LastCommitID: sourceRepoSha,
})
require.NoError(t, err)
assert.NotEmpty(t, resp)
return resp.Commit.SHA
}
func renamePullMirrorSourceRepo(t *testing.T, sourceRepo *repo_model.Repository) *repo_model.Repository {
session := loginUser(t, "user2")
apiToken := getTokenForLoggedInUser(t, session, auth.AccessTokenScopeWriteRepository)
newName := uuid.NewString()
session.MakeRequest(t,
NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/user2/%s", sourceRepo.Name),
&structs.EditRepoOption{
Name: &newName,
}).AddTokenAuth(apiToken),
http.StatusOK)
newRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: sourceRepo.ID})
assert.Equal(t, newRepo.Name, newName)
assert.NotEqual(t, newRepo.CloneLink().HTTPS, sourceRepo.CloneLink().HTTPS) // should have changed to new name
return newRepo
}
func TestPullMirrorRedactCredentials(t *testing.T) {
defer unittest.OverrideFixtures("tests/integration/fixtures/TestPullMirrorRedactCredentials")()
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user2")
session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user2/repo1001/settings", map[string]string{
"action": "mirror-sync",
}), http.StatusSeeOther)
flashCookie := session.GetCookie(app_context.CookieNameFlash)
assert.NotNil(t, flashCookie)
assert.Equal(t, "info%3DPulling%2Bchanges%2Bfrom%2Bthe%2Bremote%2Bhttps%253A%252F%252Fexample.com%252Fexample%252Fexample.git%2Bat%2Bthe%2Bmoment.", flashCookie.Value)
}