Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ func main() {
}
})))

mux.Handle("/notes/imports", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
handler.ImportNotesHandler(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})))

mux.Handle("/notes/", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
Expand Down
49 changes: 47 additions & 2 deletions backend/internal/api/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,45 @@ func (h *Handler) CreateNodeHandler(w http.ResponseWriter, r *http.Request) {
response.WriteResponse(w, http.StatusAccepted, fmt.Sprintf("%d", noteID))
}

func (db *Handler) ImportNotesHandler(w http.ResponseWriter, r *http.Request) {
db.Logger.Debug("ImportNotesHandler called")

httperr := validator.ImportNotesValidator(r)
if httperr != nil {
validator.WriteError(w, httperr)
return
}

clerkID, ok := auth.UserIDFromContext(r.Context())
if !ok {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}

note, err := request.UpdateNoteparser(r)
if err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}

noteID, err := db.Service.CreateNote(r.Context(), clerkID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

if err := db.Service.UpdateNodeById(r.Context(), note, noteID); err != nil {
if deleteErr := db.Service.DelNoteById(r.Context(), noteID); deleteErr != nil {
db.Logger.Error("failed to delete imported note after update failure", "note_id", noteID, "error", deleteErr)
}

http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

response.WriteResponse(w, http.StatusAccepted, fmt.Sprintf("%d", noteID))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

func (db *Handler) UpdateNoteHandler(w http.ResponseWriter, r *http.Request) {
db.Logger.Debug("UpdateNoteHandler called")

Expand All @@ -93,9 +132,15 @@ func (db *Handler) UpdateNoteHandler(w http.ResponseWriter, r *http.Request) {
}

note, err := request.UpdateNoteparser(r)
check(err)
if err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}

db.Service.UpdateNodeById(r.Context(), note, id)
if err := db.Service.UpdateNodeById(r.Context(), note, id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

func (db *Handler) DelNoteHandler(w http.ResponseWriter, r *http.Request) {
Expand Down
117 changes: 102 additions & 15 deletions backend/internal/api/handler/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import (
"backend/internal/api/request"
"backend/internal/db"
"context"
"errors"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"

clerk "github.com/clerk/clerk-sdk-go/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

type MockService struct {
mock.Mock

}

func (m *MockService) GetNoteById(ctx context.Context, noteID int) (db.Note, error) {
Expand Down Expand Up @@ -44,6 +46,13 @@ func (m *MockService) CreateNote(ctx context.Context, clerkid string) (int, erro
return args.Int(0), args.Error(1)
}

func authenticatedRequest(req *http.Request, userID string) *http.Request {
claims := &clerk.SessionClaims{}
claims.Subject = userID
ctx := clerk.ContextWithSessionClaims(req.Context(), claims)
return req.WithContext(ctx)
}

func TestNoteHandler(t *testing.T) {
mockService := new(MockService)

Expand All @@ -52,8 +61,8 @@ func TestNoteHandler(t *testing.T) {
Return(db.Note{ID: 1}, nil)

handler := Handler{Service: mockService,
Logger: slog.New(slog.NewTextHandler(io.Discard,nil)),
}
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

req := httptest.NewRequest(http.MethodGet, "/notes/1", nil)
w := httptest.NewRecorder()
Expand All @@ -72,8 +81,8 @@ func TestNoteListHandler(t *testing.T) {
Return([]db.Note{}, nil)

handler := Handler{Service: mockService,
Logger: slog.New(slog.NewTextHandler(io.Discard,nil)),
}
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

req := httptest.NewRequest(http.MethodGet, "/notes", nil)
w := httptest.NewRecorder()
Expand All @@ -92,34 +101,114 @@ func TestUpdateNoteHandler(t *testing.T) {
Return(nil)

handler := Handler{Service: mockService,
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

body := `{"title":"test","content":{}}`
req := httptest.NewRequest(http.MethodPatch, "/notes/1", strings.NewReader(body))
w := httptest.NewRecorder()
req.Header.Set("Content-Type", "application/json")

handler.UpdateNoteHandler(w, req)

Logger: slog.New(slog.NewTextHandler(io.Discard,nil)),
assert.Equal(t, http.StatusOK, w.Code)
mockService.AssertExpectations(t)
}

body := `{"title":"test","content":{}}`
req := httptest.NewRequest(http.MethodPatch, "/notes/1",
strings.NewReader(body))
func TestUpdateNoteHandlerRejectsInvalidJSON(t *testing.T) {
mockService := new(MockService)
handler := Handler{Service: mockService,
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

req := httptest.NewRequest(http.MethodPatch, "/notes/1", strings.NewReader(`{"title":`))
w := httptest.NewRecorder()
req.Header.Set("Content-Type", "application/json")

handler.UpdateNoteHandler(w, req)

assert.Equal(t, http.StatusBadRequest, w.Code)
mockService.AssertNotCalled(t, "UpdateNodeById", mock.Anything, mock.Anything, mock.Anything)
}

func TestUpdateNoteHandlerReturnsInternalServerErrorOnUpdateFailure(t *testing.T) {
mockService := new(MockService)
mockService.
On("UpdateNodeById", mock.Anything, mock.Anything, 1).
Return(errors.New("update failed"))

handler := Handler{Service: mockService,
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

body := `{"title":"test","content":{}}`
req := httptest.NewRequest(http.MethodPatch, "/notes/1", strings.NewReader(body))
w := httptest.NewRecorder()
req.Header.Set("Content-Type", "application/json")

handler.UpdateNoteHandler(w, req)

assert.Equal(t, http.StatusInternalServerError, w.Code)
mockService.AssertExpectations(t)
}

func TestDelNoteHandler(t *testing.T) {
func TestImportNotesHandlerRejectsInvalidJSONBeforeCreate(t *testing.T) {
mockService := new(MockService)
handler := Handler{Service: mockService,
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

req := httptest.NewRequest(http.MethodPost, "/notes/imports", strings.NewReader(`{"title":`))
req = authenticatedRequest(req, "user_123")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

handler.ImportNotesHandler(w, req)

assert.Equal(t, http.StatusBadRequest, w.Code)
mockService.AssertNotCalled(t, "CreateNote", mock.Anything, mock.Anything)
mockService.AssertNotCalled(t, "UpdateNodeById", mock.Anything, mock.Anything, mock.Anything)
mockService.AssertNotCalled(t, "DelNoteById", mock.Anything, mock.Anything)
}

func TestImportNotesHandlerDeletesCreatedNoteOnUpdateFailure(t *testing.T) {
mockService := new(MockService)
mockService.
On("DelNoteById", mock.Anything, 1).
On("CreateNote", mock.Anything, "user_123").
Return(42, nil)
mockService.
On("UpdateNodeById", mock.Anything, mock.Anything, 42).
Return(errors.New("update failed"))
mockService.
On("DelNoteById", mock.Anything, 42).
Return(nil)


handler := Handler{Service: mockService,
Logger: slog.New(slog.NewTextHandler(io.Discard,nil)),
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

body := `{"title":"test","content":{}}`
req := httptest.NewRequest(http.MethodPost, "/notes/imports", strings.NewReader(body))
req = authenticatedRequest(req, "user_123")
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()

handler.ImportNotesHandler(w, req)

assert.Equal(t, http.StatusInternalServerError, w.Code)
mockService.AssertExpectations(t)
}

func TestDelNoteHandler(t *testing.T) {
mockService := new(MockService)

mockService.
On("DelNoteById", mock.Anything, 1).
Return(nil)

handler := Handler{Service: mockService,
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
}

req := httptest.NewRequest(http.MethodDelete, "/notes/1", nil)
w := httptest.NewRecorder()
Expand All @@ -128,5 +217,3 @@ func TestDelNoteHandler(t *testing.T) {

mockService.AssertExpectations(t)
}


36 changes: 32 additions & 4 deletions backend/internal/api/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"log"
"mime"
"net/http"
"strconv"
"strings"
Expand Down Expand Up @@ -36,6 +37,17 @@ func formatpath(path string) []string {
return strings.Split(path, "/")
}

func validateJSONContentType(r *http.Request) *httpError {
contentType := r.Header.Get("Content-Type")
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil || mediaType != "application/json" {
log.Printf("invalid content-type: %s", contentType)
return &httpError{Status: http.StatusUnsupportedMediaType, Msg: "media not supported"}
}

return nil
}

func GetNoteValidator(r *http.Request) (int, *httpError) {
if err := validatehttpmethod(r, http.MethodGet); err != nil {
return 0, &httpError{Status: http.StatusBadRequest, Msg: "method is not supported"}
Expand Down Expand Up @@ -110,10 +122,8 @@ func UpdateNoteValidator(r *http.Request) (int, *httpError) {
return 0, &httpError{Status: http.StatusMethodNotAllowed, Msg: "method not supported"}
}

contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
log.Printf("invalid content-type: %s", contentType)
return 0, &httpError{Status: http.StatusUnsupportedMediaType, Msg: "media not supported"}
if err := validateJSONContentType(r); err != nil {
return 0, err
}

if r.ContentLength == 0 {
Expand All @@ -135,3 +145,21 @@ func UpdateNoteValidator(r *http.Request) (int, *httpError) {

return id, nil
}

func ImportNotesValidator(r *http.Request) *httpError {
if err := validatehttpmethod(r, http.MethodPost); err != nil {
return &httpError{Status: http.StatusMethodNotAllowed, Msg: "method not supported"}
}

if err := validateJSONContentType(r); err != nil {
return err
}

parts := formatpath(r.URL.Path)
if len(parts) != 2 || parts[0] != "notes" || parts[1] != "imports" {
log.Printf("invalid path: %s", r.URL.Path)
return &httpError{Status: http.StatusBadRequest, Msg: "path wrong"}
}

return nil
}
Loading
Loading