Compare commits
3 commits
49e14b5bc5
...
89a5fdc4e5
| Author | SHA1 | Date | |
|---|---|---|---|
| 89a5fdc4e5 | |||
| 8be785cd8b | |||
| 5a540184c9 |
10 changed files with 752 additions and 226 deletions
2
go.mod
2
go.mod
|
|
@ -10,7 +10,9 @@ require (
|
||||||
require golang.org/x/crypto v0.27.0 // indirect
|
require golang.org/x/crypto v0.27.0 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/google/go-cmp v0.5.9 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||||
|
gotest.tools/v3 v3.5.2 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
|
@ -16,3 +18,5 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
|
|
|
||||||
20
main.go
20
main.go
|
|
@ -41,13 +41,13 @@ const DB_VERSION = 1
|
||||||
|
|
||||||
const DEFAULT_PORT int64 = 8080
|
const DEFAULT_PORT int64 = 8080
|
||||||
const HRT_DATE int64 = 1756478697
|
const HRT_DATE int64 = 1756478697
|
||||||
const DEFAULT_LOG_FLAGS = log.Ldate | log.Ltime | log.Lmicroseconds
|
|
||||||
|
|
||||||
//go:embed "public"
|
//go:embed "public"
|
||||||
var publicFS embed.FS
|
var publicFS embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
logger := log.New(os.Stderr, "main", DEFAULT_LOG_FLAGS)
|
// TODO: switch to a new logger. this one kinda sucks
|
||||||
|
logger := log.New(os.Stderr, "main", model.DEFAULT_LOG_FLAGS)
|
||||||
|
|
||||||
logger.Print("made with <3 by ari melody\n\n")
|
logger.Print("made with <3 by ari melody\n\n")
|
||||||
|
|
||||||
|
|
@ -94,13 +94,13 @@ func main() {
|
||||||
logRepo := logRepo.NewLogRepositoryPostgres(psqlDB)
|
logRepo := logRepo.NewLogRepositoryPostgres(psqlDB)
|
||||||
app.Log = logService.NewLogService(
|
app.Log = logService.NewLogService(
|
||||||
logRepo,
|
logRepo,
|
||||||
log.New(os.Stderr, "logger", DEFAULT_LOG_FLAGS),
|
log.New(os.Stderr, "logger", model.DEFAULT_LOG_FLAGS),
|
||||||
)
|
)
|
||||||
|
|
||||||
accountRepo := accountRepo.NewAccountRepositoryPostgres(psqlDB)
|
accountRepo := accountRepo.NewAccountRepositoryPostgres(psqlDB)
|
||||||
app.AccountService = accountService.NewAccountService(
|
app.AccountService = accountService.NewAccountService(
|
||||||
accountRepo,
|
accountRepo,
|
||||||
log.New(os.Stderr, "account-repo", DEFAULT_LOG_FLAGS),
|
log.New(os.Stderr, "account-repo", model.DEFAULT_LOG_FLAGS),
|
||||||
)
|
)
|
||||||
|
|
||||||
// handle command arguments
|
// handle command arguments
|
||||||
|
|
@ -306,15 +306,7 @@ func main() {
|
||||||
}
|
}
|
||||||
account.Password = string(hashedPassword)
|
account.Password = string(hashedPassword)
|
||||||
|
|
||||||
var email *string = nil
|
if err = app.AccountService.ChangePassword(account.ID, string(hashedPassword)); err != nil {
|
||||||
if account.Email.Valid { email = &account.Email.String }
|
|
||||||
var avatarURL *string = nil
|
|
||||||
if account.AvatarURL.Valid { email = &account.AvatarURL.String }
|
|
||||||
if err = app.AccountService.Update(
|
|
||||||
account.ID,
|
|
||||||
username, string(hashedPassword),
|
|
||||||
email, avatarURL,
|
|
||||||
); err != nil {
|
|
||||||
logger.Fatalf("FATAL: Failed to update password: %v\n", err)
|
logger.Fatalf("FATAL: Failed to update password: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -494,7 +486,7 @@ func main() {
|
||||||
|
|
||||||
go cursor.StartCursor(&app)
|
go cursor.StartCursor(&app)
|
||||||
|
|
||||||
httpLogger := log.New(os.Stderr, "http", DEFAULT_LOG_FLAGS)
|
httpLogger := log.New(os.Stderr, "http", model.DEFAULT_LOG_FLAGS)
|
||||||
|
|
||||||
// start the web server!
|
// start the web server!
|
||||||
mux := createServeMux(&app)
|
mux := createServeMux(&app)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
LogLevel int
|
LogLevel int
|
||||||
|
|
@ -15,6 +18,8 @@ type (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
DEFAULT_LOG_FLAGS = log.Ldate | log.Ltime | log.Lmicroseconds
|
||||||
|
|
||||||
LOG_ACCOUNT string = "account"
|
LOG_ACCOUNT string = "account"
|
||||||
LOG_MUSIC string = "music"
|
LOG_MUSIC string = "music"
|
||||||
LOG_ARTIST string = "artist"
|
LOG_ARTIST string = "artist"
|
||||||
|
|
|
||||||
|
|
@ -1,157 +1,149 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"strings"
|
||||||
"time"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Release_DescriptionHTML(t *testing.T) {
|
func Test_Release(t *testing.T) {
|
||||||
release := Release{
|
t.Run("prints correct description HTML", func(t *testing.T) {
|
||||||
Description: "this is\na test\n<strong>description!</strong>",
|
release := Release{
|
||||||
}
|
Description: "this is\na test\n<strong>description!</strong>",
|
||||||
|
}
|
||||||
|
|
||||||
// descriptions are set by privileged users,
|
// descriptions are set by privileged users,
|
||||||
// so we'll allow HTML injection here
|
// so we'll allow HTML injection here
|
||||||
want := "this is<br>a test<br><strong>description!</strong>"
|
assert.Equal(
|
||||||
got := release.GetDescriptionHTML()
|
t,
|
||||||
if want != string(got) {
|
string(release.GetDescriptionHTML()),
|
||||||
t.Errorf(`release description incorrectly formatted (want "%s", got "%s")`, want, got)
|
"this is<br>a test<br><strong>description!</strong>",
|
||||||
}
|
)
|
||||||
}
|
})
|
||||||
|
|
||||||
func Test_Release_ReleaseDate(t *testing.T) {
|
t.Run("prints correct release date", func(t *testing.T) {
|
||||||
release := Release{
|
release := Release{
|
||||||
ReleaseDate: time.Date(2025, time.July, 26, 16, 0, 0, 0, time.UTC),
|
ReleaseDate: time.Date(2025, time.July, 26, 16, 0, 0, 0, time.UTC),
|
||||||
}
|
}
|
||||||
|
|
||||||
want := "2025-07-26T16:00"
|
assert.Equal(t, release.TextReleaseDate(), "2025-07-26T16:00")
|
||||||
got := release.TextReleaseDate()
|
assert.Equal(t, release.PrintReleaseDate(), "26 July 2025")
|
||||||
if want != got {
|
})
|
||||||
t.Errorf(`release date incorrectly formatted (want "%s", got "%s")`, want, got)
|
|
||||||
}
|
t.Run("returns correct artwork", func(t *testing.T) {
|
||||||
|
artwork := "testartwork.png"
|
||||||
want = "26 July 2025"
|
release := Release{ Artwork: artwork }
|
||||||
got = release.PrintReleaseDate()
|
assert.Equal(t, release.GetArtwork(), artwork)
|
||||||
if want != got {
|
})
|
||||||
t.Errorf(`release date (print) incorrectly formatted (want "%s", got "%s")`, want, got)
|
|
||||||
}
|
t.Run("returns placeholder artwork when empty", func(t *testing.T) {
|
||||||
}
|
release := Release{}
|
||||||
|
assert.Equal(t, release.GetArtwork(), "/img/default-cover-art.png")
|
||||||
func Test_Release_Artwork(t *testing.T) {
|
})
|
||||||
want := "testartwork.png"
|
|
||||||
release := Release{ Artwork: want }
|
t.Run("singles", func(t *testing.T) {
|
||||||
|
release := Release{
|
||||||
got := release.GetArtwork()
|
Tracks: []*Track{},
|
||||||
if want != got {
|
}
|
||||||
t.Errorf(`correct value not returned when artwork is populated (want "%s", got "%s")`, want, got)
|
|
||||||
}
|
t.Run("false when no tracks are present", func(t *testing.T) {
|
||||||
|
assert.Equal(t, release.IsSingle(), false)
|
||||||
release = Release{}
|
})
|
||||||
|
|
||||||
want = "/img/default-cover-art.png"
|
release.Tracks = append(release.Tracks, &Track{})
|
||||||
got = release.GetArtwork()
|
t.Run("true when one track is present", func(t *testing.T) {
|
||||||
if want != got {
|
assert.Equal(t, release.IsSingle(), true)
|
||||||
t.Errorf(`default value not returned when artwork is empty (want "%s", got "%s")`, want, got)
|
})
|
||||||
}
|
|
||||||
}
|
release.Tracks = append(release.Tracks, &Track{})
|
||||||
|
t.Run("false when >1 tracks are present", func(t *testing.T) {
|
||||||
func Test_Release_IsSingle(t *testing.T) {
|
assert.Equal(t, release.IsSingle(), false)
|
||||||
release := Release{
|
})
|
||||||
Tracks: []*Track{},
|
})
|
||||||
}
|
|
||||||
|
t.Run("released", func(t *testing.T) {
|
||||||
if release.IsSingle() {
|
release := Release {
|
||||||
t.Errorf("IsSingle() == true when no tracks are present")
|
ReleaseDate: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
release.Tracks = append(release.Tracks, &Track{})
|
t.Run("true when release date in the past", func(t *testing.T) {
|
||||||
if !release.IsSingle() {
|
assert.Equal(t, release.IsReleased(), true)
|
||||||
t.Errorf("IsSingle() == false when one track is present")
|
})
|
||||||
}
|
|
||||||
|
release.ReleaseDate = time.Now().Add(time.Hour)
|
||||||
release.Tracks = append(release.Tracks, &Track{})
|
t.Run("false when release date in the future", func(t *testing.T) {
|
||||||
if release.IsSingle() {
|
assert.Equal(t, release.IsReleased(), false)
|
||||||
t.Errorf("IsSingle() == true when >1 tracks are present")
|
})
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
t.Run("printing artists", func(t *testing.T) {
|
||||||
func Test_Release_IsReleased(t *testing.T) {
|
artist1 := "ari melody"
|
||||||
release := Release {
|
artist2 := "aridoodle"
|
||||||
ReleaseDate: time.Now(),
|
artist3 := "idk"
|
||||||
}
|
artist4 := "guest"
|
||||||
|
|
||||||
if !release.IsReleased() {
|
release := Release{}
|
||||||
t.Errorf("IsRelease() == false when release date in the past")
|
t.Run("prints \"Unknown Artist\" when release has no credits", func(t *testing.T) {
|
||||||
}
|
assert.Equal(t, release.PrintArtists(false, true), "Unknown Artist")
|
||||||
|
})
|
||||||
release.ReleaseDate = time.Now().Add(time.Hour)
|
|
||||||
if release.IsReleased() {
|
release.Credits = append(
|
||||||
t.Errorf("IsRelease() == true when release date in the future")
|
release.Credits,
|
||||||
}
|
&Credit{ Artist: Artist{ Name: artist1 }, Primary: true },
|
||||||
}
|
)
|
||||||
|
t.Run("prints ONLY first artist name when release has one credit", func(t *testing.T) {
|
||||||
func Test_Release_PrintArtists(t *testing.T) {
|
assert.Equal(t, release.PrintArtists(false, true), artist1)
|
||||||
artist1 := "ari melody"
|
})
|
||||||
artist2 := "aridoodle"
|
|
||||||
artist3 := "idk"
|
release.Credits = append(release.Credits, []*Credit{
|
||||||
artist4 := "guest"
|
{ Artist: Artist{ Name: artist2 }, Primary: true },
|
||||||
|
{ Artist: Artist{ Name: artist3 }, Primary: false },
|
||||||
release := Release {
|
{ Artist: Artist{ Name: artist4 }, Primary: true },
|
||||||
Credits: []*Credit{
|
}...)
|
||||||
{ Artist: Artist{ Name: artist1 }, Primary: true },
|
t.Run("can get only unique primary artist names", func(t *testing.T) {
|
||||||
{ Artist: Artist{ Name: artist2 }, Primary: true },
|
assert.Equal(
|
||||||
{ Artist: Artist{ Name: artist3 }, Primary: false },
|
t,
|
||||||
{ Artist: Artist{ Name: artist4 }, Primary: true },
|
strings.Join(release.GetUniqueArtistNames(true), " "),
|
||||||
},
|
strings.Join([]string{ artist1, artist2, artist4 }, " "),
|
||||||
}
|
)
|
||||||
|
})
|
||||||
{
|
t.Run("can get only unique artist names", func(t *testing.T) {
|
||||||
want := []string{ artist1, artist2, artist4 }
|
assert.Equal(
|
||||||
got := release.GetUniqueArtistNames(true)
|
t,
|
||||||
if len(want) != len(got) {
|
strings.Join(release.GetUniqueArtistNames(false), " "),
|
||||||
t.Errorf(`len(GetUniqueArtistNames) (primary only) == %d, want %d`, len(got), len(want))
|
strings.Join([]string{ artist1, artist2, artist3, artist4 }, " "),
|
||||||
}
|
)
|
||||||
for i := range got {
|
})
|
||||||
if want[i] != got[i] {
|
|
||||||
t.Errorf(`GetUniqueArtistNames[%d] (primary only) == %s, want %s`, i, got[i], want[i])
|
t.Run("can print only primary artists, with ampersands", func(t *testing.T) {
|
||||||
}
|
assert.Equal(
|
||||||
}
|
t,
|
||||||
|
release.PrintArtists(true, true),
|
||||||
want = []string{ artist1, artist2, artist3, artist4 }
|
"ari melody, aridoodle & guest",
|
||||||
got = release.GetUniqueArtistNames(false)
|
)
|
||||||
if len(want) != len(got) {
|
})
|
||||||
t.Errorf(`len(GetUniqueArtistNames) == %d, want %d`, len(got), len(want))
|
t.Run("can print only primary artists, without ampersands", func(t *testing.T) {
|
||||||
}
|
assert.Equal(
|
||||||
for i := range got {
|
t,
|
||||||
if want[i] != got[i] {
|
release.PrintArtists(true, false),
|
||||||
t.Errorf(`GetUniqueArtistNames[%d] == %s, want %s`, i, got[i], want[i])
|
"ari melody, aridoodle, guest",
|
||||||
}
|
)
|
||||||
}
|
})
|
||||||
}
|
t.Run("can print all artists, with ampersands", func(t *testing.T) {
|
||||||
|
assert.Equal(
|
||||||
{
|
t,
|
||||||
want := "ari melody, aridoodle & guest"
|
release.PrintArtists(false, true),
|
||||||
got := release.PrintArtists(true, true)
|
"ari melody, aridoodle, idk & guest",
|
||||||
if want != got {
|
)
|
||||||
t.Errorf(`PrintArtists (primary only, ampersand) == "%s", want "%s"`, want, got)
|
})
|
||||||
}
|
t.Run("can print all artists, without ampersands", func(t *testing.T) {
|
||||||
|
assert.Equal(
|
||||||
want = "ari melody, aridoodle, guest"
|
t,
|
||||||
got = release.PrintArtists(true, false)
|
release.PrintArtists(false, false),
|
||||||
if want != got {
|
"ari melody, aridoodle, idk, guest",
|
||||||
t.Errorf(`PrintArtists (primary only) == "%s", want "%s"`, want, got)
|
)
|
||||||
}
|
})
|
||||||
|
})
|
||||||
want = "ari melody, aridoodle, idk & guest"
|
|
||||||
got = release.PrintArtists(false, true)
|
|
||||||
if want != got {
|
|
||||||
t.Errorf(`PrintArtists (all, ampersand) == "%s", want "%s"`, want, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
want = "ari melody, aridoodle, idk, guest"
|
|
||||||
got = release.PrintArtists(false, false)
|
|
||||||
if want != got {
|
|
||||||
t.Errorf(`PrintArtists (all) == "%s", want "%s"`, want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,20 @@ package account
|
||||||
import "arimelody-web/model"
|
import "arimelody-web/model"
|
||||||
|
|
||||||
type AccountRepository interface {
|
type AccountRepository interface {
|
||||||
GetAll() ([]model.Account, error)
|
GetAll() ([]*model.Account, error)
|
||||||
GetCount() (int, error)
|
GetCount() (int, error)
|
||||||
GetByID(id string) (*model.Account, error)
|
GetByID(id string) (*model.Account, error)
|
||||||
GetByUsername(username string) (*model.Account, error)
|
GetByUsername(username string) (*model.Account, error)
|
||||||
GetByEmail(email string) (*model.Account, error)
|
GetByEmail(email string) (*model.Account, error)
|
||||||
GetBySession(sessionToken string) (*model.Account, error)
|
|
||||||
|
// Pulled this function: Cross-cutting concerns between accounts and sessions.
|
||||||
|
// Instead, fetch account ID from session and use GetByID()
|
||||||
|
// GetBySession(sessionToken string) (*model.Account, error)
|
||||||
|
|
||||||
// Create an account, returning the new account ID.
|
// Create an account, returning the new account ID.
|
||||||
Create(username string, password string, email *string, avatarURL *string) (string, error)
|
Create(username string, password string, email *string, avatarURL *string) (string, error)
|
||||||
|
|
||||||
// Intended for large profile updates. For smaller adjusments,
|
// Deprecated in favour of more specialised Change* and Remove* functions.
|
||||||
// more specialised Change* and Remove* functions should be used.
|
|
||||||
Update(id string, username string, password string, email *string, avatarUrl *string) error
|
Update(id string, username string, password string, email *string, avatarUrl *string) error
|
||||||
ChangeUsername(id string, username string) error
|
ChangeUsername(id string, username string) error
|
||||||
ChangePassword(id string, password string) error
|
ChangePassword(id string, password string) error
|
||||||
|
|
@ -23,11 +25,12 @@ type AccountRepository interface {
|
||||||
ChangeAvatarURL(id string, avatarURL string) error
|
ChangeAvatarURL(id string, avatarURL string) error
|
||||||
RemoveAvatar(id string) error
|
RemoveAvatar(id string) error
|
||||||
|
|
||||||
Delete(accountID string) error
|
Delete(id string) error
|
||||||
|
|
||||||
// Increment the number of account login failure attempts,
|
// Increment the number of account login failure attempts,
|
||||||
// returning the current fail count.
|
// returning the current fail count.
|
||||||
IncrementFails(accountID string) (int, error)
|
IncrementFails(id string) (int, error)
|
||||||
Lock(accountID string) error
|
ResetFails(id string) error
|
||||||
Unlock(accountID string) error
|
Lock(id string) error
|
||||||
|
Unlock(id string) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
198
repository/account/memory.go
Normal file
198
repository/account/memory.go
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/model"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
AccountRepositoryMemory struct {
|
||||||
|
accounts []*model.Account
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ AccountRepository = new(AccountRepositoryMemory)
|
||||||
|
|
||||||
|
func NewAccountRepositoryMemory() *AccountRepositoryMemory {
|
||||||
|
return &AccountRepositoryMemory{ accounts: make([]*model.Account, 0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *AccountRepositoryMemory) GetAll() ([]*model.Account, error) {
|
||||||
|
return repo.accounts, nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) GetCount() (int, error) {
|
||||||
|
return len(repo.accounts), nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) GetByID(id string) (*model.Account, error) {
|
||||||
|
for _, account := range repo.accounts {
|
||||||
|
if account.ID == id { return account, nil }
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) GetByUsername(username string) (*model.Account, error) {
|
||||||
|
for _, account := range repo.accounts {
|
||||||
|
if account.Username == username { return account, nil }
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) GetByEmail(email string) (*model.Account, error) {
|
||||||
|
for _, account := range repo.accounts {
|
||||||
|
if account.Email.Valid && account.Email.String == email {
|
||||||
|
return account, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an account, returning the new account ID.
|
||||||
|
func (repo *AccountRepositoryMemory) Create(username string, password string, email *string, avatarURL *string) (string, error) {
|
||||||
|
if account, err := repo.GetByUsername(username); err != nil {
|
||||||
|
return "", errors.New("Failed to fetch other acccounts by username")
|
||||||
|
} else if account != nil {
|
||||||
|
return "", errors.New("Account with this username already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
emailRef := ""
|
||||||
|
if email != nil { emailRef = *email }
|
||||||
|
avatarURLRef := ""
|
||||||
|
if avatarURL != nil { avatarURLRef = *avatarURL }
|
||||||
|
|
||||||
|
id := strconv.Itoa(len(repo.accounts))
|
||||||
|
|
||||||
|
repo.accounts = append(repo.accounts, &model.Account{
|
||||||
|
ID: id,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
Email: sql.NullString{ String: emailRef, Valid: email != nil },
|
||||||
|
AvatarURL: sql.NullString{ String: avatarURLRef, Valid: avatarURL != nil },
|
||||||
|
})
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intended for large profile updates. For smaller adjusments,
|
||||||
|
// more specialised Change* and Remove* functions should be used.
|
||||||
|
func (repo *AccountRepositoryMemory) Update(id string, username string, password string, email *string, avatarUrl *string) error {
|
||||||
|
if account, err := repo.GetByUsername(username); err != nil {
|
||||||
|
return errors.New("Failed to fetch other acccounts by username")
|
||||||
|
} else if account != nil && account.ID != id {
|
||||||
|
return errors.New("Account with this username already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
|
||||||
|
account.Username = username
|
||||||
|
account.Password = password
|
||||||
|
account.Email.Valid = email != nil
|
||||||
|
if account.Email.Valid { account.Email.String = *email }
|
||||||
|
account.AvatarURL.Valid = avatarUrl != nil
|
||||||
|
if account.AvatarURL.Valid { account.AvatarURL.String = *avatarUrl }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) ChangeUsername(id string, username string) error {
|
||||||
|
if account, err := repo.GetByUsername(username); err != nil {
|
||||||
|
return errors.New("Failed to fetch other acccounts by username")
|
||||||
|
} else if account != nil && account.ID != id {
|
||||||
|
return errors.New("Account with this username already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
if account == nil { return errors.New("Account does not exist") }
|
||||||
|
|
||||||
|
account.Username = username
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) ChangePassword(id string, password string) error {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
if account == nil { return errors.New("Account does not exist") }
|
||||||
|
|
||||||
|
account.Password = password
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) ChangeEmail(id string, email string) error {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
if account == nil { return errors.New("Account does not exist") }
|
||||||
|
|
||||||
|
account.Email.Valid = true
|
||||||
|
account.Email.String = email
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) RemoveEmail(id string) error {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
if account == nil { return errors.New("Account does not exist") }
|
||||||
|
|
||||||
|
account.Email.Valid = false
|
||||||
|
account.Email.String = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) ChangeAvatarURL(id string, avatarURL string) error {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
if account == nil { return errors.New("Account does not exist") }
|
||||||
|
|
||||||
|
account.AvatarURL.Valid = true
|
||||||
|
account.AvatarURL.String = avatarURL
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) RemoveAvatar(id string) error {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
if account == nil { return errors.New("Account does not exist") }
|
||||||
|
|
||||||
|
account.AvatarURL.Valid = false
|
||||||
|
account.AvatarURL.String = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *AccountRepositoryMemory) Delete(id string) error {
|
||||||
|
accountIndex := -1
|
||||||
|
for index, account := range repo.accounts {
|
||||||
|
if account.ID == id {
|
||||||
|
accountIndex = index
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if accountIndex == -1 { return nil }
|
||||||
|
|
||||||
|
repo.accounts = append(
|
||||||
|
repo.accounts[:accountIndex],
|
||||||
|
repo.accounts[accountIndex+1:]...,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the number of account login failure attempts,
|
||||||
|
// returning the current fail count.
|
||||||
|
func (repo *AccountRepositoryMemory) IncrementFails(id string) (int, error) {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return 0, err }
|
||||||
|
account.FailAttempts += 1
|
||||||
|
return account.FailAttempts, nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) ResetFails(id string) error {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
account.FailAttempts = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) Lock(id string) error {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
account.Locked = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (repo *AccountRepositoryMemory) Unlock(id string) error {
|
||||||
|
account, err := repo.GetByID(id)
|
||||||
|
if err != nil { return err }
|
||||||
|
account.Locked = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -20,8 +20,8 @@ func NewAccountRepositoryPostgres(db *sqlx.DB) *AccountRepositoryPostgres {
|
||||||
return &AccountRepositoryPostgres{ db: db }
|
return &AccountRepositoryPostgres{ db: db }
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) GetAll() ([]model.Account, error) {
|
func (repo *AccountRepositoryPostgres) GetAll() ([]*model.Account, error) {
|
||||||
var accounts = []model.Account{}
|
var accounts = []*model.Account{}
|
||||||
|
|
||||||
err := repo.db.Select(&accounts, "SELECT * FROM account ORDER BY created_at ASC")
|
err := repo.db.Select(&accounts, "SELECT * FROM account ORDER BY created_at ASC")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -79,22 +79,6 @@ func (repo *AccountRepositoryPostgres) GetByEmail(email string) (*model.Account,
|
||||||
return &account, nil
|
return &account, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) GetBySession(sessionToken string) (*model.Account, error) {
|
|
||||||
if sessionToken == "" { return nil, nil }
|
|
||||||
|
|
||||||
account := model.Account{}
|
|
||||||
|
|
||||||
err := repo.db.Get(&account, "SELECT account.* FROM account JOIN token ON id=account WHERE token=$1", sessionToken)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) Create(
|
func (repo *AccountRepositoryPostgres) Create(
|
||||||
username string,
|
username string,
|
||||||
password string,
|
password string,
|
||||||
|
|
@ -175,25 +159,30 @@ func (repo *AccountRepositoryPostgres) RemoveAvatar(id string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) Delete(accountID string) error {
|
func (repo *AccountRepositoryPostgres) Delete(id string) error {
|
||||||
_, err := repo.db.Exec("DELETE FROM account WHERE id=$1", accountID)
|
_, err := repo.db.Exec("DELETE FROM account WHERE id=$1", id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the number of account login failure attempts,
|
// Increment the number of account login failure attempts,
|
||||||
// returning the current fail count.
|
// returning the current fail count.
|
||||||
func (repo *AccountRepositoryPostgres) IncrementFails(accountID string) (int, error) {
|
func (repo *AccountRepositoryPostgres) IncrementFails(id string) (int, error) {
|
||||||
failAttempts := 0
|
failAttempts := 0
|
||||||
err := repo.db.Get(&failAttempts, "UPDATE account SET fail_attempts = fail_attempts + 1 WHERE id=$1 RETURNING fail_attempts", accountID)
|
err := repo.db.Get(&failAttempts, "UPDATE account SET fail_attempts = fail_attempts + 1 WHERE id=$1 RETURNING fail_attempts", id)
|
||||||
return failAttempts, err
|
return failAttempts, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) Lock(accountID string) error {
|
func (repo *AccountRepositoryPostgres) ResetFails(id string) error {
|
||||||
_, err := repo.db.Exec("UPDATE account SET locked = true WHERE id=$1", accountID)
|
_, err := repo.db.Exec("UPDATE account SET fail_attempts = 0 WHERE id=$1", id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) Unlock(accountID string) error {
|
func (repo *AccountRepositoryPostgres) Lock(id string) error {
|
||||||
_, err := repo.db.Exec("UPDATE account SET locked = false, fail_attempts = 0 WHERE id=$1", accountID)
|
_, err := repo.db.Exec("UPDATE account SET locked = true WHERE id=$1", id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repo *AccountRepositoryPostgres) Unlock(id string) error {
|
||||||
|
_, err := repo.db.Exec("UPDATE account SET locked = false, fail_attempts = 0 WHERE id=$1", id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func NewAccountService(repo repository.AccountRepository, logger *log.Logger) (*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AccountService) GetAll() ([]model.Account, error) {
|
func (s *AccountService) GetAll() ([]*model.Account, error) {
|
||||||
return s.repo.GetAll()
|
return s.repo.GetAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,12 +39,6 @@ func (s *AccountService) GetByEmail(email string) (*model.Account, error) {
|
||||||
return s.repo.GetByEmail(email)
|
return s.repo.GetByEmail(email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AccountService) GetBySession(sessionToken string) (*model.Account, error) {
|
|
||||||
if sessionToken == "" { return nil, nil }
|
|
||||||
|
|
||||||
return s.repo.GetBySession(sessionToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) Create(
|
func (s *AccountService) Create(
|
||||||
username string,
|
username string,
|
||||||
password string,
|
password string,
|
||||||
|
|
@ -66,26 +60,6 @@ func (s *AccountService) Create(
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intended for large profile updates. For smaller adjusments,
|
|
||||||
// more specialised Change* and Remove* functions should be used.
|
|
||||||
func (s *AccountService) Update(
|
|
||||||
id string,
|
|
||||||
username string,
|
|
||||||
password string,
|
|
||||||
email *string,
|
|
||||||
avatarUrl *string,
|
|
||||||
) error {
|
|
||||||
if len(username) == 0 { return errors.New("Username cannot be empty") }
|
|
||||||
if len(password) == 0 { return errors.New("Password cannot be empty") }
|
|
||||||
if email != nil && len(*email) == 0 { return errors.New("Email cannot be empty") }
|
|
||||||
|
|
||||||
if err := s.repo.Update(id, username, password, email, avatarUrl); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.log.Printf("Updated account '%s' (%s)", username, id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *AccountService) ChangeUsername(id string, username string) error {
|
func (s *AccountService) ChangeUsername(id string, username string) error {
|
||||||
if len(username) == 0 { return errors.New("Username cannot be empty") }
|
if len(username) == 0 { return errors.New("Username cannot be empty") }
|
||||||
if err := s.repo.ChangeUsername(id, username); err != nil {
|
if err := s.repo.ChangeUsername(id, username); err != nil {
|
||||||
|
|
@ -131,6 +105,10 @@ func (s *AccountService) IncrementFails(accountID string) (int, error) {
|
||||||
return s.repo.IncrementFails(accountID)
|
return s.repo.IncrementFails(accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AccountService) ResetFails(accountID string) (error) {
|
||||||
|
return s.repo.ResetFails(accountID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *AccountService) Lock(accountID string) error {
|
func (s *AccountService) Lock(accountID string) error {
|
||||||
return s.repo.Lock(accountID)
|
return s.repo.Lock(accountID)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
363
service/account/account_test.go
Normal file
363
service/account/account_test.go
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/model"
|
||||||
|
accountRepo "arimelody-web/repository/account"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"gotest.tools/v3/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
service *AccountService
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
devNullFile, err := os.OpenFile(os.DevNull, os.O_RDWR, 0666)
|
||||||
|
if err != nil { panic(err) }
|
||||||
|
defer devNullFile.Close()
|
||||||
|
|
||||||
|
repo := accountRepo.NewAccountRepositoryMemory()
|
||||||
|
service = NewAccountService(
|
||||||
|
repo,
|
||||||
|
log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests the account lifecycle. Users:
|
||||||
|
// - [x] can create account
|
||||||
|
// - [x] can't create duplicate account
|
||||||
|
// - [x] can change username
|
||||||
|
// - [x] can change password
|
||||||
|
// - [x] can change email
|
||||||
|
// - [x] can change avatar URL
|
||||||
|
// - [x] can increment auth failures
|
||||||
|
// - [x] can reset auth failures
|
||||||
|
// - [x] can lock account
|
||||||
|
// - [x] can unlock account
|
||||||
|
// - [x] can delete account
|
||||||
|
func Test_Account(t *testing.T) {
|
||||||
|
username := "testificate"
|
||||||
|
password := "the amazing digital data breach"
|
||||||
|
email := "goober@arimelody.space"
|
||||||
|
avatarURL := "/img/default-avatar.webp"
|
||||||
|
|
||||||
|
var id string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
t.Run("accounts should start empty", func(t *testing.T) {
|
||||||
|
t.Run("count is zero", func(t *testing.T) {
|
||||||
|
if num, err := service.GetCount(); err != nil {
|
||||||
|
t.Errorf("Failed to get number of accounts: %v", err)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, num, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("service returns empty array", func(t *testing.T) {
|
||||||
|
if accounts, err := service.GetAll(); err != nil {
|
||||||
|
t.Errorf("Failed to get number of accounts: %v", err)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, len(accounts), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can create account", func(t *testing.T) {
|
||||||
|
id, err = service.Create(username, password, &email, &avatarURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to create account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("and fetch by ID", func(t *testing.T) {
|
||||||
|
account, err := service.GetByID(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to get account after creation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, account.Username, username)
|
||||||
|
assert.Equal(t, account.Password, password)
|
||||||
|
assert.Equal(t, account.Email.String, email)
|
||||||
|
assert.Equal(t, account.AvatarURL.String, avatarURL)
|
||||||
|
assert.Equal(t, account.FailAttempts, 0)
|
||||||
|
assert.Equal(t, account.Locked, false)
|
||||||
|
})
|
||||||
|
t.Run("and fetch by username", func(t *testing.T) {
|
||||||
|
account, err := service.GetByUsername(username)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to get account after creation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, account.ID, id)
|
||||||
|
assert.Equal(t, account.Password, password)
|
||||||
|
assert.Equal(t, account.Email.String, email)
|
||||||
|
assert.Equal(t, account.AvatarURL.String, avatarURL)
|
||||||
|
assert.Equal(t, account.FailAttempts, 0)
|
||||||
|
assert.Equal(t, account.Locked, false)
|
||||||
|
})
|
||||||
|
t.Run("and fetch by email", func(t *testing.T) {
|
||||||
|
account, err := service.GetByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Failed to get account after creation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, account.ID, id)
|
||||||
|
assert.Equal(t, account.Username, username)
|
||||||
|
assert.Equal(t, account.Password, password)
|
||||||
|
assert.Equal(t, account.AvatarURL.String, avatarURL)
|
||||||
|
assert.Equal(t, account.FailAttempts, 0)
|
||||||
|
assert.Equal(t, account.Locked, false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("number of accounts should increment", func(t *testing.T) {
|
||||||
|
t.Run("count is one", func(t *testing.T) {
|
||||||
|
if num, err := service.GetCount(); err != nil {
|
||||||
|
t.Errorf("Failed to get number of accounts: %v", err)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, num, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("service returns array with one account", func(t *testing.T) {
|
||||||
|
if accounts, err := service.GetAll(); err != nil {
|
||||||
|
t.Errorf("Failed to get number of accounts: %v", err)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, len(accounts), 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can't create duplicate account", func(t *testing.T) {
|
||||||
|
_, err := service.Create(username, password, &email, &avatarURL)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Duplicate account was created")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can change username", func(t *testing.T) {
|
||||||
|
testUsername := "some_other_name"
|
||||||
|
if err := service.ChangeUsername(id, testUsername); err != nil {
|
||||||
|
t.Errorf("Failed to change username: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else if account.Username != testUsername {
|
||||||
|
t.Error("Username did not update")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("but not to an invalid value", func(t *testing.T) {
|
||||||
|
if err := service.ChangeUsername(id, ""); err == nil {
|
||||||
|
t.Error("Could change username to invalid value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can change password", func(t *testing.T) {
|
||||||
|
testPassword := "other more different password"
|
||||||
|
if err := service.ChangePassword(id, testPassword); err != nil {
|
||||||
|
t.Errorf("Failed to change password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else if account.Password != testPassword {
|
||||||
|
t.Error("Password did not update")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("but not to an invalid value", func(t *testing.T) {
|
||||||
|
if err := service.ChangePassword(id, ""); err == nil {
|
||||||
|
t.Error("Could change password to invalid value")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can change email", func(t *testing.T) {
|
||||||
|
testEmail := "brandnewemail@for.me"
|
||||||
|
if err := service.ChangeEmail(id, testEmail); err != nil {
|
||||||
|
t.Errorf("Failed to change email: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else if !account.Email.Valid || account.Email.String != testEmail {
|
||||||
|
t.Error("Email did not update")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can remove email", func(t *testing.T) {
|
||||||
|
if err := service.ChangeEmail(id, ""); err != nil {
|
||||||
|
t.Errorf("Failed to change email: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else if account.Email.Valid || len(account.Email.String) > 0 {
|
||||||
|
t.Error("Email did not update")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can change avatar URL", func(t *testing.T) {
|
||||||
|
testAvatarURL := "/img/some-other-avatar.webp"
|
||||||
|
if err := service.ChangeAvatarURL(id, testAvatarURL); err != nil {
|
||||||
|
t.Errorf("Failed to change avatar URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else if !account.AvatarURL.Valid || account.AvatarURL.String != testAvatarURL {
|
||||||
|
t.Error("Avatar URL did not update")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can remove avatar URL", func(t *testing.T) {
|
||||||
|
if err := service.ChangeAvatarURL(id, ""); err != nil {
|
||||||
|
t.Errorf("Failed to change avatar URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else if account.AvatarURL.Valid || len(account.AvatarURL.String) > 0 {
|
||||||
|
t.Error("Avatar URL did not update")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can increment auth failures", func(t *testing.T) {
|
||||||
|
if num, err := service.IncrementFails(id); err != nil {
|
||||||
|
t.Errorf("Failed to increment account auth failures: %v", err)
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, num, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, account.FailAttempts, 1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can reset auth failures", func(t *testing.T) {
|
||||||
|
if err := service.ResetFails(id); err != nil {
|
||||||
|
t.Errorf("Failed to reset account auth failures: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, account.FailAttempts, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can lock account", func(t *testing.T) {
|
||||||
|
if err := service.Lock(id); err != nil {
|
||||||
|
t.Errorf("Failed to lock account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, account.Locked, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can unlock account", func(t *testing.T) {
|
||||||
|
if err := service.Unlock(id); err != nil {
|
||||||
|
t.Errorf("Failed to unlock account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account: %v", err)
|
||||||
|
} else if account == nil {
|
||||||
|
t.Error("Account is nil after update")
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, account.Locked, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can delete account", func(t *testing.T) {
|
||||||
|
if err = service.Delete(id); err != nil {
|
||||||
|
t.Errorf("Failed to delete account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if account, err := service.GetByID(id); err != nil {
|
||||||
|
t.Errorf("Failed to get account after deletion: %v", err)
|
||||||
|
} else if account != nil {
|
||||||
|
t.Error("Account still exists after deletion")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can't create an account with invalid", func(t *testing.T) {
|
||||||
|
t.Run("username", func(t *testing.T) {
|
||||||
|
if _, err := service.Create("", password, &email, &avatarURL); err == nil {
|
||||||
|
t.Error("Could create account with empty username")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("password", func(t *testing.T) {
|
||||||
|
if _, err := service.Create(username, "", &email, &avatarURL); err == nil {
|
||||||
|
t.Error("Could create account with empty password")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("email", func(t *testing.T) {
|
||||||
|
testEmail := ""
|
||||||
|
if _, err := service.Create(username, password, &testEmail, &avatarURL); err == nil {
|
||||||
|
t.Error("Could create account with empty (non-nil) email")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("can't update account that doesn't exist", func(t *testing.T) {
|
||||||
|
garbageAccountID := "adsginh534g9405gmb40i9bm"
|
||||||
|
|
||||||
|
t.Run("username", func(t *testing.T) {
|
||||||
|
if err := service.ChangeUsername(garbageAccountID, "some-username"); err == nil {
|
||||||
|
t.Error("Could update non-existent account's username")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("password", func(t *testing.T) {
|
||||||
|
if err := service.ChangePassword(garbageAccountID, "some-password"); err == nil {
|
||||||
|
t.Error("Could update non-existent account's password")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("email", func(t *testing.T) {
|
||||||
|
if err := service.ChangeEmail(garbageAccountID, "some-email@real.gov"); err == nil {
|
||||||
|
t.Error("Could update non-existent account's email")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("avatar URL", func(t *testing.T) {
|
||||||
|
if err := service.ChangeAvatarURL(garbageAccountID, "/img/null.webp"); err == nil {
|
||||||
|
t.Error("Could update non-existent account's avatar URL")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := service.Delete(id); err != nil {
|
||||||
|
t.Errorf("Failed to clean up test case: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue