diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3d870a0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.23.4 + +WORKDIR /app + +COPY go.mod go.sum ./ + +RUN go mod tidy + +COPY . . + +RUN go build -o notesApp . + +EXPOSE 8080 + +CMD ["./notesApp"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1de4ab0..835541f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,5 +10,20 @@ services: - "${APP_DB_PORT}:5432" volumes: - postgres_data:/var/lib/postgresql/data + + app: + build: . + container_name: notes_app + depends_on: + - db + environment: + DB_HOST: db + DB_PORT: 5432 + DB_USER: ${APP_DB_USER} + DB_PASSWORD: ${APP_DB_PASSWORD} + DB_NAME: ${APP_DB_NAME} + ports: + - "8080:8080" + volumes: postgres_data: \ No newline at end of file diff --git a/go.mod b/go.mod index a85ff72..b46199b 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,19 @@ module NotesWebApp go 1.23.4 require ( + github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/gorilla/mux v1.8.1 github.com/gorilla/sessions v1.4.0 github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + github.com/stretchr/testify v1.10.0 golang.org/x/crypto v0.32.0 ) -require github.com/gorilla/securecookie v1.1.2 // indirect +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 74eb695..fea3dbc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= @@ -14,9 +18,18 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/auth_handler.go b/handlers/auth_handler.go index 1627cf0..00b43a3 100644 --- a/handlers/auth_handler.go +++ b/handlers/auth_handler.go @@ -49,6 +49,7 @@ func (ah *AuthHandler) Login(w http.ResponseWriter, r *http.Request) { user, err := models.GetUserByEmail(ah.DB, email) if err != nil { http.Error(w, "User not found: invalid credentials", http.StatusUnauthorized) + return } if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { diff --git a/main.go b/main.go index 11db717..b394f9d 100644 --- a/main.go +++ b/main.go @@ -65,7 +65,6 @@ func main() { IdleTimeout: 30 * time.Second, } - log.Println("Server is running on port 8080...") if err := runServer(db, server); err != nil { log.Fatal("Server error:", err) } diff --git a/models/note_test.go b/models/note_test.go new file mode 100644 index 0000000..b2bbd62 --- /dev/null +++ b/models/note_test.go @@ -0,0 +1,209 @@ +package models + +import ( + "database/sql" + "errors" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" +) + +func TestNote_CreateNote(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + note := &Note{ + Title: "Test Title", + Content: "Test Content", + UserID: 1, + } + + rows := sqlmock.NewRows([]string{"id", "created_at", "updated_at"}). + AddRow(1, time.Now(), time.Now()) + + mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO notes (title, content, user_id) +VALUES ($1, $2, $3) RETURNING id, created_at, updated_at`)). + WithArgs(note.Title, note.Content, note.UserID). + WillReturnRows(rows) + + err = note.CreateNote(sqlxDB) + assert.NoError(t, err) + + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestNote_UpdateNote(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Faild to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + note := &Note{ + ID: 1, + Title: "Updated Title", + Content: "Updated Content", + UpdatedAt: time.Now(), + } + + mock.ExpectExec(regexp.QuoteMeta(`UPDATE notes SET title=?, content=?, updated_at=? + WHERE id=?`)). + WithArgs(note.Title, note.Content, sqlmock.AnyArg(), note.ID). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err = note.UpdateNote(sqlxDB) + assert.NoError(t, err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestNote_DeleteNote(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + note := &Note{ + ID: 1, + } + + mock.ExpectExec(regexp.QuoteMeta(`DELETE FROM notes WHERE id=?`)).WithArgs(note.ID). + WillReturnResult(sqlmock.NewResult(1, 1)) + + err = note.DeleteNote(sqlxDB) + assert.NoError(t, err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetNotesByUser(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + userID := 1 + expectedNotes := []Note{ + {ID: 1, Title: "Note 1", Content: "Content 1", CreatedAt: time.Now(), UpdatedAt: time.Now()}, + {ID: 2, Title: "Note 2", Content: "Content 2", CreatedAt: time.Now(), UpdatedAt: time.Now()}, + } + + rows := sqlmock.NewRows([]string{"id", "title", "content", "created_at", "updated_at"}). + AddRow(expectedNotes[0].ID, expectedNotes[0].Title, expectedNotes[0].Content, expectedNotes[0].CreatedAt, + expectedNotes[0].UpdatedAt). + AddRow(expectedNotes[1].ID, expectedNotes[1].Title, expectedNotes[1].Content, expectedNotes[1].CreatedAt, + expectedNotes[1].UpdatedAt) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, title, content, created_at, updated_at +FROM notes WHERE user_id=$1`)). + WithArgs(userID). + WillReturnRows(rows) + + note := &Note{} + + notes, err := note.GetNotesByUser(sqlxDB, userID) + + assert.NoError(t, err) + assert.Equal(t, expectedNotes, notes) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetNoteByID(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + noteID := 1 + expectedNote := &Note{ + ID: noteID, + Title: "Test Note", + Content: "Test Content", + UserID: 1, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + rows := sqlmock.NewRows([]string{"id", "title", "content", "user_id", "created_at", "updated_at"}). + AddRow(expectedNote.ID, expectedNote.Title, expectedNote.Content, expectedNote.UserID, + time.Now(), time.Now()) + + mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, title, content, user_id, created_at, updated_at +FROM notes WHERE id=$1`)). + WithArgs(noteID). + WillReturnRows(rows) + + note, err := GetNoteByID(sqlxDB, noteID) + assert.NoError(t, err) + assert.NotNil(t, note) + + assert.Equal(t, expectedNote.ID, noteID) + assert.Equal(t, expectedNote.Title, note.Title) + assert.Equal(t, expectedNote.Content, note.Content) + assert.Equal(t, expectedNote.UserID, note.UserID) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetNoteByID_Error(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + noteID := 1 + expectedError := errors.New("database connection error") + mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, title, content, user_id, created_at, updated_at +FROM notes WHERE id=$1`)). + WithArgs(noteID). + WillReturnError(expectedError) + + note, err := GetNoteByID(sqlxDB, noteID) + assert.Error(t, err) + assert.Nil(t, note) + assert.Equal(t, expectedError, err) + + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetNoteByID_NotFound(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + userID := 1 + mock.ExpectQuery(regexp.QuoteMeta(`SELECT id, title, content, user_id, created_at, updated_at +FROM notes WHERE id=$1`)). + WithArgs(userID). + WillReturnError(sql.ErrNoRows) + + note, err := GetNoteByID(sqlxDB, userID) + assert.NoError(t, err) + assert.Nil(t, note) + + assert.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/models/user.go b/models/user.go index aeff2db..990deaf 100644 --- a/models/user.go +++ b/models/user.go @@ -11,7 +11,7 @@ type User struct { } func (u *User) CreateUser(db *sqlx.DB) error { - query := `INSERT into users (email, password) VALUES ($1, $2) RETURNING id` + query := `INSERT INTO users (email, password) VALUES ($1, $2) RETURNING id` return db.QueryRowx(query, u.Email, u.Password).Scan(&u.ID) } diff --git a/models/user_test.go b/models/user_test.go new file mode 100644 index 0000000..46b5d39 --- /dev/null +++ b/models/user_test.go @@ -0,0 +1,63 @@ +package models + +import ( + "github.com/DATA-DOG/go-sqlmock" + "github.com/jmoiron/sqlx" + "github.com/stretchr/testify/assert" + "regexp" + "testing" +) + +func TestUser_CreateUser(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + user := &User{ + Email: "test@example.com", + Password: "hashedpassword", + } + + rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO users (email, password) VALUES ($1, $2) RETURNING id`)). + WithArgs(user.Email, user.Password). + WillReturnRows(rows) + + err = user.CreateUser(sqlxDB) + assert.NoError(t, err) + assert.NoError(t, mock.ExpectationsWereMet()) +} + +func TestGetUserByEmail(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("Failed to create sqlmock: %v", err) + } + defer db.Close() + + sqlxDB := sqlx.NewDb(db, "sqlmock") + + email := "test@example.com" + expectedUser := &User{ + ID: 1, + Email: email, + Password: "hashedpassword", + } + + rows := sqlmock.NewRows([]string{"id", "email", "password"}). + AddRow(expectedUser.ID, expectedUser.Email, expectedUser.Password) + + mock.ExpectQuery(`SELECT id, email, password FROM users WHERE email=\$1`). + WithArgs(email). + WillReturnRows(rows) + + user, err := GetUserByEmail(sqlxDB, email) + assert.NoError(t, err) + assert.Equal(t, expectedUser, user) + assert.NoError(t, mock.ExpectationsWereMet()) +} diff --git a/notesApp b/notesApp new file mode 100755 index 0000000..996dcab Binary files /dev/null and b/notesApp differ