mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-04-13 11:50:34 +02:00
refactor: use t instead of tt (#2000)
Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>
This commit is contained in:
parent
49c17befcf
commit
873f35a2c2
@ -86,7 +86,7 @@ func TestWithDefaults(t *testing.T) {
|
|||||||
goBinary: "go",
|
goBinary: "go",
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(name, func(tt *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
var config = config.Project{
|
var config = config.Project{
|
||||||
Builds: []config.Build{
|
Builds: []config.Build{
|
||||||
testcase.build,
|
testcase.build,
|
||||||
@ -621,12 +621,12 @@ func TestInvalidTemplate(t *testing.T) {
|
|||||||
"{{ .Nope }": `template: tmpl:1: unexpected "}" in operand`,
|
"{{ .Nope }": `template: tmpl:1: unexpected "}" in operand`,
|
||||||
"{{.Env.NOPE}}": `template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`,
|
"{{.Env.NOPE}}": `template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`,
|
||||||
} {
|
} {
|
||||||
t.Run(template, func(tt *testing.T) {
|
t.Run(template, func(t *testing.T) {
|
||||||
var ctx = context.New(config.Project{})
|
var ctx = context.New(config.Project{})
|
||||||
ctx.Git.CurrentTag = "3.4.1"
|
ctx.Git.CurrentTag = "3.4.1"
|
||||||
flags, err := tmpl.New(ctx).Apply(template)
|
flags, err := tmpl.New(ctx).Apply(template)
|
||||||
require.EqualError(tt, err, eerr)
|
require.EqualError(t, err, eerr)
|
||||||
require.Empty(tt, flags)
|
require.Empty(t, flags)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ func createFakeBinary(t *testing.T, dist, arch, bin string) {
|
|||||||
func TestRunPipe(t *testing.T) {
|
func TestRunPipe(t *testing.T) {
|
||||||
var folder = testlib.Mktmp(t)
|
var folder = testlib.Mktmp(t)
|
||||||
for _, format := range []string{"tar.gz", "zip"} {
|
for _, format := range []string{"tar.gz", "zip"} {
|
||||||
t.Run("Archive format "+format, func(tt *testing.T) {
|
t.Run("Archive format "+format, func(t *testing.T) {
|
||||||
var dist = filepath.Join(folder, format+"_dist")
|
var dist = filepath.Join(folder, format+"_dist")
|
||||||
require.NoError(t, os.Mkdir(dist, 0755))
|
require.NoError(t, os.Mkdir(dist, 0755))
|
||||||
for _, arch := range []string{"darwinamd64", "linux386", "linuxarm7", "linuxmipssoftfloat"} {
|
for _, arch := range []string{"darwinamd64", "linux386", "linuxarm7", "linuxmipssoftfloat"} {
|
||||||
@ -137,7 +137,7 @@ func TestRunPipe(t *testing.T) {
|
|||||||
ctx.Version = "0.0.1"
|
ctx.Version = "0.0.1"
|
||||||
ctx.Git.CurrentTag = "v0.0.1"
|
ctx.Git.CurrentTag = "v0.0.1"
|
||||||
ctx.Config.Archives[0].Format = format
|
ctx.Config.Archives[0].Format = format
|
||||||
require.NoError(tt, Pipe{}.Run(ctx))
|
require.NoError(t, Pipe{}.Run(ctx))
|
||||||
var archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableArchive)).List()
|
var archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableArchive)).List()
|
||||||
for _, arch := range archives {
|
for _, arch := range archives {
|
||||||
require.Equal(t, "myid", arch.Extra["ID"].(string), "all archives should have the archive ID set")
|
require.Equal(t, "myid", arch.Extra["ID"].(string), "all archives should have the archive ID set")
|
||||||
@ -676,7 +676,7 @@ func TestBinaryOverride(t *testing.T) {
|
|||||||
_, err = os.Create(filepath.Join(folder, "README.md"))
|
_, err = os.Create(filepath.Join(folder, "README.md"))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
for _, format := range []string{"tar.gz", "zip"} {
|
for _, format := range []string{"tar.gz", "zip"} {
|
||||||
t.Run("Archive format "+format, func(tt *testing.T) {
|
t.Run("Archive format "+format, func(t *testing.T) {
|
||||||
var ctx = context.New(
|
var ctx = context.New(
|
||||||
config.Project{
|
config.Project{
|
||||||
Dist: dist,
|
Dist: dist,
|
||||||
@ -725,17 +725,17 @@ func TestBinaryOverride(t *testing.T) {
|
|||||||
ctx.Version = "0.0.1"
|
ctx.Version = "0.0.1"
|
||||||
ctx.Config.Archives[0].Format = format
|
ctx.Config.Archives[0].Format = format
|
||||||
|
|
||||||
require.NoError(tt, Pipe{}.Run(ctx))
|
require.NoError(t, Pipe{}.Run(ctx))
|
||||||
var archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableArchive))
|
var archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableArchive))
|
||||||
darwin := archives.Filter(artifact.ByGoos("darwin")).List()[0]
|
darwin := archives.Filter(artifact.ByGoos("darwin")).List()[0]
|
||||||
require.Equal(tt, "foobar_0.0.1_darwin_amd64."+format, darwin.Name)
|
require.Equal(t, "foobar_0.0.1_darwin_amd64."+format, darwin.Name)
|
||||||
require.Equal(tt, format, darwin.ExtraOr("Format", ""))
|
require.Equal(t, format, darwin.ExtraOr("Format", ""))
|
||||||
require.Empty(tt, darwin.ExtraOr("WrappedIn", ""))
|
require.Empty(t, darwin.ExtraOr("WrappedIn", ""))
|
||||||
|
|
||||||
archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableBinary))
|
archives = ctx.Artifacts.Filter(artifact.ByType(artifact.UploadableBinary))
|
||||||
windows := archives.Filter(artifact.ByGoos("windows")).List()[0]
|
windows := archives.Filter(artifact.ByGoos("windows")).List()[0]
|
||||||
require.Equal(tt, "foobar_0.0.1_windows_amd64.exe", windows.Name)
|
require.Equal(t, "foobar_0.0.1_windows_amd64.exe", windows.Name)
|
||||||
require.Empty(tt, windows.ExtraOr("WrappedIn", ""))
|
require.Empty(t, windows.ExtraOr("WrappedIn", ""))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -756,17 +756,17 @@ func TestRunPipeNoUpload(t *testing.T) {
|
|||||||
testlib.AssertSkipped(t, doRun(ctx, ctx.Config.Brews[0], client))
|
testlib.AssertSkipped(t, doRun(ctx, ctx.Config.Brews[0], client))
|
||||||
require.False(t, client.CreatedFile)
|
require.False(t, client.CreatedFile)
|
||||||
}
|
}
|
||||||
t.Run("skip upload", func(tt *testing.T) {
|
t.Run("skip upload", func(t *testing.T) {
|
||||||
ctx.Config.Release.Draft = false
|
ctx.Config.Release.Draft = false
|
||||||
ctx.Config.Brews[0].SkipUpload = "true"
|
ctx.Config.Brews[0].SkipUpload = "true"
|
||||||
ctx.SkipPublish = false
|
ctx.SkipPublish = false
|
||||||
assertNoPublish(tt)
|
assertNoPublish(t)
|
||||||
})
|
})
|
||||||
t.Run("skip publish", func(tt *testing.T) {
|
t.Run("skip publish", func(t *testing.T) {
|
||||||
ctx.Config.Release.Draft = false
|
ctx.Config.Release.Draft = false
|
||||||
ctx.Config.Brews[0].SkipUpload = "false"
|
ctx.Config.Brews[0].SkipUpload = "false"
|
||||||
ctx.SkipPublish = true
|
ctx.SkipPublish = true
|
||||||
assertNoPublish(tt)
|
assertNoPublish(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ func TestPipeInvalidNameTemplate(t *testing.T) {
|
|||||||
"{{ .Pro }_checksums.txt": `template: tmpl:1: unexpected "}" in operand`,
|
"{{ .Pro }_checksums.txt": `template: tmpl:1: unexpected "}" in operand`,
|
||||||
"{{.Env.NOPE}}": `template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`,
|
"{{.Env.NOPE}}": `template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`,
|
||||||
} {
|
} {
|
||||||
t.Run(template, func(tt *testing.T) {
|
t.Run(template, func(t *testing.T) {
|
||||||
var folder = t.TempDir()
|
var folder = t.TempDir()
|
||||||
var ctx = context.New(
|
var ctx = context.New(
|
||||||
config.Project{
|
config.Project{
|
||||||
@ -168,8 +168,8 @@ func TestPipeInvalidNameTemplate(t *testing.T) {
|
|||||||
Path: binFile.Name(),
|
Path: binFile.Name(),
|
||||||
})
|
})
|
||||||
err = Pipe{}.Run(ctx)
|
err = Pipe{}.Run(ctx)
|
||||||
require.Error(tt, err)
|
require.Error(t, err)
|
||||||
require.Equal(tt, eerr, err.Error())
|
require.Equal(t, eerr, err.Error())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -810,15 +810,15 @@ func TestRunPipe(t *testing.T) {
|
|||||||
defer killAndRm(t)
|
defer killAndRm(t)
|
||||||
|
|
||||||
for name, docker := range table {
|
for name, docker := range table {
|
||||||
t.Run(name, func(tt *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
var folder = t.TempDir()
|
var folder = t.TempDir()
|
||||||
var dist = filepath.Join(folder, "dist")
|
var dist = filepath.Join(folder, "dist")
|
||||||
require.NoError(tt, os.Mkdir(dist, 0755))
|
require.NoError(t, os.Mkdir(dist, 0755))
|
||||||
require.NoError(tt, os.Mkdir(filepath.Join(dist, "mybin"), 0755))
|
require.NoError(t, os.Mkdir(filepath.Join(dist, "mybin"), 0755))
|
||||||
_, err := os.Create(filepath.Join(dist, "mybin", "mybin"))
|
_, err := os.Create(filepath.Join(dist, "mybin", "mybin"))
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
_, err = os.Create(filepath.Join(dist, "mybin", "anotherbin"))
|
_, err = os.Create(filepath.Join(dist, "mybin", "anotherbin"))
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var ctx = context.New(config.Project{
|
var ctx = context.New(config.Project{
|
||||||
ProjectName: "mybin",
|
ProjectName: "mybin",
|
||||||
@ -862,21 +862,21 @@ func TestRunPipe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = Pipe{}.Run(ctx)
|
err = Pipe{}.Run(ctx)
|
||||||
docker.assertError(tt, err)
|
docker.assertError(t, err)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
docker.pubAssertError(tt, Pipe{}.Publish(ctx))
|
docker.pubAssertError(t, Pipe{}.Publish(ctx))
|
||||||
docker.manifestAssertError(tt, ManifestPipe{}.Publish(ctx))
|
docker.manifestAssertError(t, ManifestPipe{}.Publish(ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, d := range docker.dockers {
|
for _, d := range docker.dockers {
|
||||||
docker.assertImageLabels(tt, len(d.ImageTemplates))
|
docker.assertImageLabels(t, len(d.ImageTemplates))
|
||||||
}
|
}
|
||||||
|
|
||||||
// this might should not fail as the image should have been created when
|
// this might should not fail as the image should have been created when
|
||||||
// the step ran
|
// the step ran
|
||||||
for _, img := range docker.expect {
|
for _, img := range docker.expect {
|
||||||
tt.Log("removing docker image", img)
|
t.Log("removing docker image", img)
|
||||||
require.NoError(tt, exec.Command("docker", "rmi", img).Run(), "could not delete image %s", img)
|
require.NoError(t, exec.Command("docker", "rmi", img).Run(), "could not delete image %s", img)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
38
internal/pipe/env/env_test.go
vendored
38
internal/pipe/env/env_test.go
vendored
@ -16,14 +16,14 @@ func TestDescription(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSetDefaultTokenFiles(t *testing.T) {
|
func TestSetDefaultTokenFiles(t *testing.T) {
|
||||||
t.Run("empty config", func(tt *testing.T) {
|
t.Run("empty config", func(t *testing.T) {
|
||||||
ctx := context.New(config.Project{})
|
ctx := context.New(config.Project{})
|
||||||
setDefaultTokenFiles(ctx)
|
setDefaultTokenFiles(ctx)
|
||||||
require.Equal(t, "~/.config/goreleaser/github_token", ctx.Config.EnvFiles.GitHubToken)
|
require.Equal(t, "~/.config/goreleaser/github_token", ctx.Config.EnvFiles.GitHubToken)
|
||||||
require.Equal(t, "~/.config/goreleaser/gitlab_token", ctx.Config.EnvFiles.GitLabToken)
|
require.Equal(t, "~/.config/goreleaser/gitlab_token", ctx.Config.EnvFiles.GitLabToken)
|
||||||
require.Equal(t, "~/.config/goreleaser/gitea_token", ctx.Config.EnvFiles.GiteaToken)
|
require.Equal(t, "~/.config/goreleaser/gitea_token", ctx.Config.EnvFiles.GiteaToken)
|
||||||
})
|
})
|
||||||
t.Run("custom config config", func(tt *testing.T) {
|
t.Run("custom config config", func(t *testing.T) {
|
||||||
cfg := "what"
|
cfg := "what"
|
||||||
ctx := context.New(config.Project{
|
ctx := context.New(config.Project{
|
||||||
EnvFiles: config.EnvFiles{
|
EnvFiles: config.EnvFiles{
|
||||||
@ -187,43 +187,43 @@ func TestInvalidEnvReleaseDisabled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadEnv(t *testing.T) {
|
func TestLoadEnv(t *testing.T) {
|
||||||
t.Run("env exists", func(tt *testing.T) {
|
t.Run("env exists", func(t *testing.T) {
|
||||||
var env = "SUPER_SECRET_ENV"
|
var env = "SUPER_SECRET_ENV"
|
||||||
require.NoError(tt, os.Setenv(env, "1"))
|
require.NoError(t, os.Setenv(env, "1"))
|
||||||
v, err := loadEnv(env, "nope")
|
v, err := loadEnv(env, "nope")
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
require.Equal(tt, "1", v)
|
require.Equal(t, "1", v)
|
||||||
})
|
})
|
||||||
t.Run("env file exists", func(tt *testing.T) {
|
t.Run("env file exists", func(t *testing.T) {
|
||||||
var env = "SUPER_SECRET_ENV_NOPE"
|
var env = "SUPER_SECRET_ENV_NOPE"
|
||||||
require.NoError(tt, os.Unsetenv(env))
|
require.NoError(t, os.Unsetenv(env))
|
||||||
f, err := ioutil.TempFile(t.TempDir(), "token")
|
f, err := ioutil.TempFile(t.TempDir(), "token")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
fmt.Fprintf(f, "123")
|
fmt.Fprintf(f, "123")
|
||||||
v, err := loadEnv(env, f.Name())
|
v, err := loadEnv(env, f.Name())
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
require.Equal(tt, "123", v)
|
require.Equal(t, "123", v)
|
||||||
})
|
})
|
||||||
t.Run("env file with an empty line at the end", func(tt *testing.T) {
|
t.Run("env file with an empty line at the end", func(t *testing.T) {
|
||||||
var env = "SUPER_SECRET_ENV_NOPE"
|
var env = "SUPER_SECRET_ENV_NOPE"
|
||||||
require.NoError(tt, os.Unsetenv(env))
|
require.NoError(t, os.Unsetenv(env))
|
||||||
f, err := ioutil.TempFile(t.TempDir(), "token")
|
f, err := ioutil.TempFile(t.TempDir(), "token")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
fmt.Fprintf(f, "123\n")
|
fmt.Fprintf(f, "123\n")
|
||||||
v, err := loadEnv(env, f.Name())
|
v, err := loadEnv(env, f.Name())
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
require.Equal(tt, "123", v)
|
require.Equal(t, "123", v)
|
||||||
})
|
})
|
||||||
t.Run("env file is not readable", func(tt *testing.T) {
|
t.Run("env file is not readable", func(t *testing.T) {
|
||||||
var env = "SUPER_SECRET_ENV_NOPE"
|
var env = "SUPER_SECRET_ENV_NOPE"
|
||||||
require.NoError(tt, os.Unsetenv(env))
|
require.NoError(t, os.Unsetenv(env))
|
||||||
f, err := ioutil.TempFile(t.TempDir(), "token")
|
f, err := ioutil.TempFile(t.TempDir(), "token")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
fmt.Fprintf(f, "123")
|
fmt.Fprintf(f, "123")
|
||||||
err = os.Chmod(f.Name(), 0377)
|
err = os.Chmod(f.Name(), 0377)
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
v, err := loadEnv(env, f.Name())
|
v, err := loadEnv(env, f.Name())
|
||||||
require.EqualError(tt, err, fmt.Sprintf("open %s: permission denied", f.Name()))
|
require.EqualError(t, err, fmt.Sprintf("open %s: permission denied", f.Name()))
|
||||||
require.Equal(tt, "", v)
|
require.Equal(t, "", v)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -374,8 +374,8 @@ func TestSignArtifacts(t *testing.T) {
|
|||||||
test.user = user
|
test.user = user
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run(test.desc, func(tt *testing.T) {
|
t.Run(test.desc, func(t *testing.T) {
|
||||||
testSign(tt, test.ctx, test.signaturePaths, test.signatureNames, test.user, test.expectedErrMsg)
|
testSign(t, test.ctx, test.signaturePaths, test.signatureNames, test.user, test.expectedErrMsg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,8 +49,8 @@ func TestWithArtifact(t *testing.T) {
|
|||||||
} {
|
} {
|
||||||
tmpl := tmpl
|
tmpl := tmpl
|
||||||
expect := expect
|
expect := expect
|
||||||
t.Run(expect, func(tt *testing.T) {
|
t.Run(expect, func(t *testing.T) {
|
||||||
tt.Parallel()
|
t.Parallel()
|
||||||
result, err := New(ctx).WithArtifact(
|
result, err := New(ctx).WithArtifact(
|
||||||
&artifact.Artifact{
|
&artifact.Artifact{
|
||||||
Name: "not-this-binary",
|
Name: "not-this-binary",
|
||||||
@ -64,13 +64,13 @@ func TestWithArtifact(t *testing.T) {
|
|||||||
},
|
},
|
||||||
map[string]string{"linux": "Linux"},
|
map[string]string{"linux": "Linux"},
|
||||||
).Apply(tmpl)
|
).Apply(tmpl)
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
require.Equal(tt, expect, result)
|
require.Equal(t, expect, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("artifact with gitlab ArtifactUploadHash", func(tt *testing.T) {
|
t.Run("artifact with gitlab ArtifactUploadHash", func(t *testing.T) {
|
||||||
tt.Parallel()
|
t.Parallel()
|
||||||
uploadHash := "820ead5d9d2266c728dce6d4d55b6460"
|
uploadHash := "820ead5d9d2266c728dce6d4d55b6460"
|
||||||
result, err := New(ctx).WithArtifact(
|
result, err := New(ctx).WithArtifact(
|
||||||
&artifact.Artifact{
|
&artifact.Artifact{
|
||||||
@ -83,12 +83,12 @@ func TestWithArtifact(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}, map[string]string{},
|
}, map[string]string{},
|
||||||
).Apply("{{ .ArtifactUploadHash }}")
|
).Apply("{{ .ArtifactUploadHash }}")
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
require.Equal(tt, uploadHash, result)
|
require.Equal(t, uploadHash, result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("artifact without binary name", func(tt *testing.T) {
|
t.Run("artifact without binary name", func(t *testing.T) {
|
||||||
tt.Parallel()
|
t.Parallel()
|
||||||
result, err := New(ctx).WithArtifact(
|
result, err := New(ctx).WithArtifact(
|
||||||
&artifact.Artifact{
|
&artifact.Artifact{
|
||||||
Name: "another-binary",
|
Name: "another-binary",
|
||||||
@ -97,15 +97,15 @@ func TestWithArtifact(t *testing.T) {
|
|||||||
Goarm: "6",
|
Goarm: "6",
|
||||||
}, map[string]string{},
|
}, map[string]string{},
|
||||||
).Apply("{{ .Binary }}")
|
).Apply("{{ .Binary }}")
|
||||||
require.NoError(tt, err)
|
require.NoError(t, err)
|
||||||
require.Equal(tt, ctx.Config.ProjectName, result)
|
require.Equal(t, ctx.Config.ProjectName, result)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("template using artifact Fields with no artifact", func(tt *testing.T) {
|
t.Run("template using artifact Fields with no artifact", func(t *testing.T) {
|
||||||
tt.Parallel()
|
t.Parallel()
|
||||||
result, err := New(ctx).Apply("{{ .Os }}")
|
result, err := New(ctx).Apply("{{ .Os }}")
|
||||||
require.EqualError(tt, err, `template: tmpl:1:3: executing "tmpl" at <.Os>: map has no entry for key "Os"`)
|
require.EqualError(t, err, `template: tmpl:1:3: executing "tmpl" at <.Os>: map has no entry for key "Os"`)
|
||||||
require.Empty(tt, result)
|
require.Empty(t, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user