diff --git a/core/debug.go b/core/debug.go new file mode 100644 index 0000000..c0139e4 --- /dev/null +++ b/core/debug.go @@ -0,0 +1,55 @@ +package lishwist + +import ( + "database/sql" + "fmt" + "log" +) + +func PrintViews(d *sql.DB) { + rows, err := d.Query("SELECT name FROM sqlite_master WHERE type = 'view';") + if err != nil { + log.Println("Query failed: %w", err) + return + } + defer rows.Close() + fmt.Println("Printing view names...") + for rows.Next() { + var name string + err = rows.Scan(&name) + if err != nil { + log.Println("Scan failed: %w", err) + return + } + fmt.Printf("name: %s\n", name) + } + err = rows.Err() + if err != nil { + log.Println("Rows returned error: %w", err) + return + } +} + +func PrintTables(d *sql.DB) { + rows, err := d.Query("SELECT name FROM sqlite_master WHERE type = 'table';") + if err != nil { + log.Println("Query failed: %w", err) + return + } + defer rows.Close() + fmt.Println("Printing table names...") + for rows.Next() { + var name string + err = rows.Scan(&name) + if err != nil { + fmt.Println("Scan failed: %w", err) + return + } + fmt.Printf("name: %s\n", name) + } + err = rows.Err() + if err != nil { + log.Println("Rows returned error: %w", err) + return + } +} diff --git a/core/go.mod b/core/go.mod index 6b195fd..d1b9821 100644 --- a/core/go.mod +++ b/core/go.mod @@ -1,3 +1,18 @@ module lishwist/core -go 1.23 +go 1.23.0 + +toolchain go1.23.3 + +require golang.org/x/crypto v0.39.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/ncruces/go-sqlite3 v0.26.1 +) + +require ( + github.com/ncruces/julianday v1.0.0 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect + golang.org/x/sys v0.33.0 // indirect +) diff --git a/core/go.sum b/core/go.sum new file mode 100644 index 0000000..f2fefe5 --- /dev/null +++ b/core/go.sum @@ -0,0 +1,14 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/ncruces/go-sqlite3 v0.26.1 h1:lBXmbmucH1Bsj57NUQR6T84UoMN7jnNImhF+ibEITJU= +github.com/ncruces/go-sqlite3 v0.26.1/go.mod h1:XFTPtFIo1DmGCh+XVP8KGn9b/o2f+z0WZuT09x2N6eo= +github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= +github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= diff --git a/core/group.go b/core/group.go new file mode 100644 index 0000000..86b1f80 --- /dev/null +++ b/core/group.go @@ -0,0 +1,130 @@ +package lishwist + +import ( + "fmt" + "lishwist/core/internal/db" + "log" + "strconv" + "strings" +) + +type Group struct { + Id string + Name string + Reference string + Members []User +} + +func (g *Group) MemberIndex(userId string) int { + for i, u := range g.Members { + if u.Id == userId { + return i + } + } + return -1 +} + +func queryManyGroups(query string, args ...any) ([]Group, error) { + groups := []Group{} + // PrintTables() + // PrintViews() + log.Println(query, args) + rows, err := db.Connection.Query(query, args...) + // PrintTables() + // PrintViews() + if err != nil { + return nil, fmt.Errorf("Query failed: %w", err) + } + defer rows.Close() + for rows.Next() { + var group Group + err := rows.Scan(&group.Id, &group.Name, &group.Reference) + if err != nil { + return nil, fmt.Errorf("Failed to scan row: %w", err) + } + members, err := queryManyGroupMembers(group.Id) + if err != nil { + return nil, fmt.Errorf("Failed to query for group members: %w", err) + } + group.Members = members + groups = append(groups, group) + } + err = rows.Err() + if err != nil { + return nil, fmt.Errorf("Rows error: %w", err) + } + return groups, nil +} + +func queryOneGroup(query string, args ...any) (*Group, error) { + groups, err := queryManyGroups(query, args...) + if err != nil { + return nil, err + } + if len(groups) < 1 { + return nil, nil + } + return &groups[0], nil +} + +func queryManyGroupMembers(groupId string) ([]User, error) { + query := "SELECT user.id, user.name, user.display_name, user.reference, user.is_admin, user.is_live FROM v_user AS user JOIN group_member ON group_member.user_id = user.id JOIN [group] ON [group].id = group_member.group_id WHERE [group].id = ? ORDER BY group_member.user_id" + members, err := queryManyUsers(query, groupId) + if err != nil { + return members, err + } + return members, nil +} + +func (a *Admin) GetGroupByReference(reference string) (*Group, error) { + query := "SELECT [group].id, [group].name, [group].reference FROM [group] WHERE [group].reference = ?;" + return queryOneGroup(query, reference) +} + +func (s *Session) GetGroupByReference(reference string) (*Group, error) { + stmt := "SELECT [group].id, [group].name, [group].reference FROM [group] JOIN group_member ON [group].id == group_member.group_id WHERE [group].reference = ? AND group_member.user_id = ?;" + return queryOneGroup(stmt, reference, s.User.Id) +} + +func (a *Admin) ListGroups() ([]Group, error) { + query := "SELECT id, name, reference FROM [group];" + return queryManyGroups(query) +} + +func (a *Admin) CreateGroup(name string, reference string) (*Group, error) { + name = strings.TrimSpace(name) + reference = strings.TrimSpace(reference) + stmt := "INSERT INTO [group] (name, reference) VALUES (?, ?)" + result, err := db.Connection.Exec(stmt, name, reference) + if err != nil { + return nil, err + } + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + group := Group{ + Id: strconv.FormatInt(id, 10), + Name: name, + Reference: reference, + } + return &group, nil +} + +func (a *Admin) AddUserToGroup(userId, groupId string) error { + stmt := "INSERT INTO group_member (user_id, group_id) VALUES (?, ?)" + _, err := db.Connection.Exec(stmt, userId, groupId) + if err != nil { + return err + } + return nil +} + +func (a *Admin) RemoveUserFromGroup(userId, groupId string) error { + stmt := "DELETE FROM group_member WHERE group_id = ? AND user_id = ?" + _, err := db.Connection.Exec(stmt, userId, groupId) + if err != nil { + return err + } + return nil +} diff --git a/core/group_test.go b/core/group_test.go new file mode 100644 index 0000000..de96b63 --- /dev/null +++ b/core/group_test.go @@ -0,0 +1,42 @@ +package lishwist_test + +import ( + "testing" + + lishwist "lishwist/core" + "lishwist/core/internal/fixtures" +) + +func TestCreateGroup(t *testing.T) { + s := fixtures.Login(t, "thomas", "123") + + group, err := s.Admin().CreateGroup(" My Friends ", " my-friends ") + fixtures.FailIfErr(t, err, "Failed to create group") + + fixtures.AssertEq(t, "Number of users", "My Friends", group.Name) + fixtures.AssertEq(t, "Number of users", "my-friends", group.Reference) +} + +func TestCantSeeSelfInGroup(t *testing.T) { + s := fixtures.Login(t, "thomas", "123") + + caleb, err := lishwist.Register("caleb", "123") + fixtures.FailIfErr(t, err, "Failed to register caleb") + + group, err := s.Admin().CreateGroup(" My Friends ", " my-friends ") + fixtures.FailIfErr(t, err, "Failed to create group") + + err = s.Admin().AddUserToGroup(s.User.Id, group.Id) + fixtures.FailIfErr(t, err, "Failed to add self to group") + + err = s.Admin().AddUserToGroup(caleb.Id, group.Id) + fixtures.FailIfErr(t, err, "Failed to add caleb to group") + + group, err = s.GetGroupByReference("my-friends") + fixtures.FailIfErr(t, err, "Failed to get group") + fixtures.FatalAssert(t, "Group not nil", group != nil) + + fixtures.AssertEq(t, "Group contains 2 users", 2, len(group.Members)) + fixtures.AssertEq(t, "Group user 1 is thomas", "thomas", group.Members[0].Name) + fixtures.AssertEq(t, "Group user 2 is caleb", "caleb", group.Members[1].Name) +} diff --git a/core/internal/db/db.go b/core/internal/db/db.go index 79517d3..938dc12 100644 --- a/core/internal/db/db.go +++ b/core/internal/db/db.go @@ -6,13 +6,14 @@ import ( "database/sql" "fmt" - _ "github.com/glebarez/go-sqlite" + _ "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" ) var Connection *sql.DB func Init(dataSourceName string) error { - db, err := sql.Open("sqlite", dataSourceName) + db, err := sql.Open("sqlite3", dataSourceName) if err != nil { return fmt.Errorf("Failed to open db connection: %w", err) } diff --git a/core/internal/fixtures/assert.go b/core/internal/fixtures/assert.go index 35d288f..39ea2a1 100644 --- a/core/internal/fixtures/assert.go +++ b/core/internal/fixtures/assert.go @@ -14,6 +14,12 @@ func Assert(t *testing.T, context string, condition bool) { } } +func FatalAssert(t *testing.T, context string, condition bool) { + if !condition { + t.Fatalf("%s", context) + } +} + func FailIfErr(t *testing.T, err error, context string) { if err != nil { t.Fatalf("%s: %s\n", context, err) diff --git a/core/internal/fixtures/login.go b/core/internal/fixtures/login.go index abff84c..d2e015e 100644 --- a/core/internal/fixtures/login.go +++ b/core/internal/fixtures/login.go @@ -2,20 +2,29 @@ package fixtures import ( "log" + "testing" "time" lishwist "lishwist/core" + + "github.com/ncruces/go-sqlite3/vfs/memdb" ) -func Login(username, password string) *lishwist.Session { - err := lishwist.Init(":memory:") +func TestInit(t *testing.T) error { + uri := memdb.TestDB(t) + return lishwist.Init(uri) +} + +func Login(t *testing.T, username, password string) *lishwist.Session { + uri := memdb.TestDB(t) + err := lishwist.Init(uri) if err != nil { log.Fatalf("Failed to init db: %s\n", err) } lw := lishwist.NewSessionManager(time.Second*10, 32) - err = lishwist.Register(username, password) + _, err = lishwist.Register(username, password) if err != nil { log.Fatalf("Failed to register on login fixture: %s\n", err) } diff --git a/core/login_test.go b/core/login_test.go index b5a107c..eca5ac0 100644 --- a/core/login_test.go +++ b/core/login_test.go @@ -5,17 +5,18 @@ import ( "time" lishwist "lishwist/core" + "lishwist/core/internal/fixtures" ) func TestLogin(t *testing.T) { - err := lishwist.Init(":memory:") + err := fixtures.TestInit(t) if err != nil { t.Fatalf("Failed to init db: %s\n", err) } lw := lishwist.NewSessionManager(time.Second*10, 32) - err = lishwist.Register("thomas", "123") + _, err = lishwist.Register("thomas", "123") if err != nil { t.Fatalf("Failed to register: %s\n", err) } diff --git a/core/register.go b/core/register.go index 5ec8432..bc33707 100644 --- a/core/register.go +++ b/core/register.go @@ -7,33 +7,33 @@ import ( "golang.org/x/crypto/bcrypt" ) -func Register(username, newPassword string) error { +func Register(username, newPassword string) (*User, error) { if username == "" { - return errors.New("Username required") + return nil, errors.New("Username required") } if newPassword == "" { - return errors.New("newPassword required") + return nil, errors.New("newPassword required") } existingUser, _ := getUserByName(username) if existingUser != nil { - return errors.New("Username is taken") + return nil, errors.New("Username is taken") } hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.MinCost) if err != nil { - return fmt.Errorf("Failed to hash password: %w", err) + return nil, fmt.Errorf("Failed to hash password: %w", err) } usersExist, err := hasUsers() if err != nil { - return fmt.Errorf("Failed to count users: %w", err) + return nil, fmt.Errorf("Failed to count users: %w", err) } - _, err = createUser(username, hashedPasswordBytes, !usersExist) + user, err := createUser(username, hashedPasswordBytes, !usersExist) if err != nil { - return fmt.Errorf("Failed to create user: %w\n", err) + return nil, fmt.Errorf("Failed to create user: %w\n", err) } - return nil + return user, nil } diff --git a/core/session.go b/core/session.go index 08f7702..5a02181 100644 --- a/core/session.go +++ b/core/session.go @@ -40,11 +40,11 @@ func (sm *SessionManager) createSession(user *User) (*Session, error) { stmt := "INSERT INTO session (user_id) VALUES (?);" result, err := db.Connection.Exec(stmt, user.Id) if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to execute query: %w", err) } id, err := result.LastInsertId() if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to get last insert id: %w", err) } token, err := generateSecureToken(sm.sessionTokenLength) diff --git a/core/user_test.go b/core/user_test.go index 6d53d5b..978ec7c 100644 --- a/core/user_test.go +++ b/core/user_test.go @@ -8,9 +8,9 @@ import ( ) func TestFirstUserIsAdmin(t *testing.T) { - s := fixtures.Login("thomas", "123") + s := fixtures.Login(t, "thomas", "123") - err := lishwist.Register("caleb", "123") + _, err := lishwist.Register("caleb", "123") fixtures.FailIfErr(t, err, "Failed to register caleb") users, err := s.Admin().ListUsers() diff --git a/core/wish_test.go b/core/wish_test.go index b867b7f..b7a950c 100644 --- a/core/wish_test.go +++ b/core/wish_test.go @@ -7,7 +7,7 @@ import ( ) func TestMakeWish(t *testing.T) { - s := fixtures.Login("thomas", "123") + s := fixtures.Login(t, "thomas", "123") if err := s.MakeWish("apple"); err != nil { t.Fatalf("Failed to make wish 1: %s\n", err) diff --git a/go.work b/go.work deleted file mode 100644 index 4e0d860..0000000 --- a/go.work +++ /dev/null @@ -1,7 +0,0 @@ -go 1.23 - -toolchain go1.23.3 - -use ./core - -use ./http diff --git a/go.work.sum b/go.work.sum deleted file mode 100644 index 2c1030c..0000000 --- a/go.work.sum +++ /dev/null @@ -1,18 +0,0 @@ -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= -modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=