-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
17 changed files
with
1,048 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package data | ||
|
||
import ( | ||
"github.com/addetz/secure-code-go/demo4/db" | ||
"github.com/google/uuid" | ||
) | ||
|
||
type SecretNote struct { | ||
ID string `json:"id"` | ||
Username string `json:"username"` | ||
Text string `json:"text"` | ||
} | ||
|
||
// SecretNoteService maintains the user notes. | ||
type SecretNoteService struct { | ||
dbService db.DatabaseService | ||
} | ||
|
||
// NewSecretNoteService creates a SecretNoteService that is ready to use. | ||
func NewSecretNoteService(dbService db.DatabaseService) *SecretNoteService { | ||
return &SecretNoteService{ | ||
dbService: dbService, | ||
} | ||
} | ||
|
||
// Add adds a new SecretNote for the given user by using the SecretNoteService. | ||
func (ns *SecretNoteService) Add(user string, n SecretNote) error { | ||
id := uuid.New().String() | ||
return ns.dbService.AddNote(id, user, n.Text) | ||
} | ||
|
||
// Get returns all the SecretNotes of a given user by using the SecretNoteService. | ||
func (ns *SecretNoteService) GetAll(user string) ([]SecretNote, error) { | ||
dbNotes, err := ns.dbService.GetUserNotes(user) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var notes []SecretNote | ||
for _, n := range dbNotes { | ||
notes = append(notes, SecretNote{ | ||
ID: n.ID, | ||
Username: n.Username, | ||
Text: n.Text, | ||
}) | ||
} | ||
|
||
return notes, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package data_test | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
"github.com/addetz/secure-code-go/demo4/data" | ||
"github.com/addetz/secure-code-go/demo4/db" | ||
"github.com/addetz/secure-code-go/demo4/mocks" | ||
"github.com/google/uuid" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
) | ||
|
||
func TestAddNote(t *testing.T) { | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
notes := data.NewSecretNoteService(mockDB) | ||
user := "user1" | ||
note := data.SecretNote{ | ||
Text: "My Secret Note", | ||
} | ||
mockDB.On("AddNote", mock.AnythingOfType("string"), user, note.Text).Return(nil) | ||
err := notes.Add(user, note) | ||
assert.Nil(t, err) | ||
} | ||
|
||
func TestGetAllNotes(t *testing.T) { | ||
t.Run("no notes found", func(t *testing.T) { | ||
user := "user1" | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
noteService := data.NewSecretNoteService(mockDB) | ||
mockDB.On("GetUserNotes", user).Return(nil, errors.New("no notes found")) | ||
notes, err := noteService.GetAll(user) | ||
assert.Nil(t, notes) | ||
assert.NotNil(t, err) | ||
assert.Contains(t, err.Error(), "no notes found") | ||
}) | ||
t.Run("notes found", func(t *testing.T) { | ||
user := "user1" | ||
dbNotes := []db.Note{ | ||
{ | ||
ID: uuid.New().String(), | ||
Username: user, | ||
Text: "My first note", | ||
}, | ||
{ | ||
ID: uuid.New().String(), | ||
Username: user, | ||
Text: "My second note", | ||
}, | ||
} | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
noteService := data.NewSecretNoteService(mockDB) | ||
mockDB.On("GetUserNotes", user).Return(dbNotes, nil) | ||
notes, err := noteService.GetAll(user) | ||
assert.Nil(t, err) | ||
assert.NotNil(t, notes) | ||
assert.Equal(t, len(dbNotes), len(notes)) | ||
for i, n := range dbNotes { | ||
assert.Equal(t, n.ID, notes[i].ID) | ||
assert.Equal(t, n.Username, notes[i].Username) | ||
assert.Equal(t, n.Text, notes[i].Text) | ||
|
||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package data | ||
|
||
import ( | ||
"github.com/addetz/secure-code-go/demo4/db" | ||
"github.com/pkg/errors" | ||
|
||
passwordvalidator "github.com/wagslane/go-password-validator" | ||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
const minEntropyBits = 60 | ||
|
||
type User struct { | ||
Username, Password string | ||
} | ||
|
||
// UserService holds | ||
type UserService struct { | ||
dbService db.DatabaseService | ||
} | ||
|
||
// NewUserService creates a ready to use user service. | ||
func NewUserService(dbService db.DatabaseService) *UserService { | ||
return &UserService{ | ||
dbService: dbService, | ||
} | ||
} | ||
|
||
// Add validates a user password and creates a new user. | ||
func (us *UserService) Add(name, password string) error { | ||
if _, err := us.dbService.GetUser(name); err == nil { | ||
return errors.New("user exists already, please log in instead") | ||
} | ||
|
||
err := passwordvalidator.Validate(password, minEntropyBits) | ||
if err != nil { | ||
return errors.Wrap(err, "validate new user password") | ||
} | ||
|
||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||
if err != nil { | ||
return err | ||
} | ||
return us.dbService.AddUser(name, string(hashedPassword)) | ||
} | ||
|
||
// ValidatePassword checks the provided password of an existing user. | ||
func (us *UserService) ValidatePassword(name, providedPwd string) error { | ||
user, err := us.dbService.GetUser(name) | ||
if err != nil { | ||
return errors.Wrap(err, "user does not exist") | ||
} | ||
return bcrypt.CompareHashAndPassword([]byte(user.Pwd), []byte(providedPwd)) | ||
} | ||
|
||
// ValidateUser checks the provided username belongs to an existing user. | ||
func (us *UserService) ValidateUser(name string) error { | ||
if _, err := us.dbService.GetUser(name); err != nil { | ||
return errors.New("user not found") | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package data_test | ||
|
||
import ( | ||
"errors" | ||
"testing" | ||
|
||
"github.com/addetz/secure-code-go/demo4/data" | ||
"github.com/addetz/secure-code-go/demo4/db" | ||
"github.com/addetz/secure-code-go/demo4/mocks" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
"golang.org/x/crypto/bcrypt" | ||
) | ||
|
||
func TestAdd(t *testing.T) { | ||
t.Run("insufficient password", func(t *testing.T) { | ||
name := "user1" | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
us := data.NewUserService(mockDB) | ||
mockDB.On("GetUser", name).Return(nil, errors.New("no user exists")) | ||
err := us.Add(name, "test") | ||
assert.NotNil(t, err) | ||
assert.Contains(t, err.Error(), "validate new user password: insecure password") | ||
}) | ||
t.Run("successful add", func(t *testing.T) { | ||
name := "user1" | ||
password := "test-horse-pen-clam" | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
us := data.NewUserService(mockDB) | ||
mockDB.On("GetUser", name).Return(nil, errors.New("no user exists")) | ||
mockDB.On("AddUser", name, mock.AnythingOfType("string")).Return(nil) | ||
err := us.Add(name, password) | ||
assert.Nil(t, err) | ||
}) | ||
t.Run("duplicate user", func(t *testing.T) { | ||
name := "user1" | ||
password := "test-horse-pen-clam" | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
us := data.NewUserService(mockDB) | ||
mockDB.On("GetUser", name).Return(nil, nil) | ||
err := us.Add(name, password) | ||
assert.NotNil(t, err) | ||
assert.Contains(t, err.Error(), "user exists already") | ||
}) | ||
} | ||
|
||
func TestValidate(t *testing.T) { | ||
t.Run("successful validate", func(t *testing.T) { | ||
name := "user1" | ||
password := "test-horse-pen-clam" | ||
expected, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
us := data.NewUserService(mockDB) | ||
mockDB.On("GetUser", name).Return(&db.User{ | ||
Username: name, | ||
Pwd: string(expected), | ||
}, nil) | ||
err := us.ValidatePassword(name, password) | ||
assert.Nil(t, err) | ||
}) | ||
t.Run("failed validate", func(t *testing.T) { | ||
name := "user1" | ||
password := "test-horse-pen-clam" | ||
expected, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
mockDB.On("GetUser", name).Return(&db.User{ | ||
Username: name, | ||
Pwd: string(expected), | ||
}, nil) | ||
us := data.NewUserService(mockDB) | ||
err := us.ValidatePassword(name, "garbage-password") | ||
assert.NotNil(t, err) | ||
assert.Contains(t, err.Error(), "hashedPassword is not the hash of the given password") | ||
}) | ||
t.Run("inexistent user", func(t *testing.T) { | ||
name := "user1" | ||
mockDB := new(mocks.DatabaseServiceMock) | ||
us := data.NewUserService(mockDB) | ||
mockDB.On("GetUser", name).Return(nil, errors.New("no user exists")) | ||
err := us.ValidatePassword(name, "garbage-password") | ||
assert.NotNil(t, err) | ||
assert.Contains(t, err.Error(), "user does not exist") | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package db | ||
|
||
import ( | ||
"database/sql" | ||
"log" | ||
) | ||
|
||
type User struct { | ||
Username, Pwd string | ||
} | ||
|
||
type Note struct { | ||
ID, Username, Text string | ||
} | ||
|
||
type dbService struct { | ||
db *sql.DB | ||
} | ||
|
||
type DatabaseService interface { | ||
AddUser(username, pwd string) error | ||
GetUser(username string) (*User, error) | ||
AddNote(id, username, text string) error | ||
GetUserNotes(username string) ([]Note, error) | ||
} | ||
|
||
// NewDatabaseService initialises a DatabaseService given its dependencies. | ||
func NewDatabaseService(db *sql.DB) *dbService { | ||
return &dbService{ | ||
db: db, | ||
} | ||
} | ||
|
||
// AddUser creates a new user in the DB | ||
func (ds *dbService) AddUser(username, pwd string) error { | ||
stmt, err := ds.db.Prepare("INSERT INTO users (username, pwd) VALUES( $1, $2 )") | ||
if err != nil { | ||
log.Println("error1", err) | ||
return err | ||
} | ||
defer stmt.Close() | ||
if _, err := stmt.Exec(username, pwd); err != nil { | ||
log.Println("error2", err) | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// GetUser returns a user from the database or an error if none exists. | ||
func (ds *dbService) GetUser(username string) (*User, error) { | ||
var user User | ||
stmt, err := ds.db.Prepare("SELECT * FROM users WHERE username = $1 ") | ||
if err != nil { | ||
log.Println("error3", err) | ||
return nil, err | ||
} | ||
defer stmt.Close() | ||
if err := stmt.QueryRow(username).Scan(&user.Username, &user.Pwd); err != nil { | ||
log.Println("error4", err) | ||
return nil, err | ||
} | ||
return &user, nil | ||
} | ||
|
||
// AddNote creates a new note in the DB | ||
func (ds *dbService) AddNote(id, username, text string) error { | ||
stmt, err := ds.db.Prepare("INSERT INTO notes(id, username, noteText) VALUES($1, $2, $3)") | ||
if err != nil { | ||
return err | ||
} | ||
defer stmt.Close() | ||
if _, err := stmt.Exec(id, username, text); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// GetUserNotes returns all the notes of a given user from the database or an error. | ||
func (ds *dbService) GetUserNotes(username string) ([]Note, error) { | ||
var notes []Note | ||
stmt, err := ds.db.Prepare("SELECT * FROM notes WHERE username = $1") | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer stmt.Close() | ||
rows, err := stmt.Query(username) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer rows.Close() | ||
for rows.Next() { | ||
n := Note{} | ||
if err := rows.Scan(&n.ID, &n.Username, &n.Text); err != nil { | ||
return nil, err | ||
} | ||
notes = append(notes, n) | ||
} | ||
return notes, nil | ||
} |
Oops, something went wrong.