Skip to content

Commit bf5ac62

Browse files
authored
Merge pull request #9 from oshankkkk/feat/import-files
Feat: import files from disk
2 parents 070f683 + e8e44f8 commit bf5ac62

10 files changed

Lines changed: 379 additions & 56 deletions

File tree

backend/cmd/server/main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ func main() {
8181
}
8282
})))
8383

84+
mux.Handle("/notes/imports", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
85+
switch r.Method {
86+
case http.MethodPost:
87+
handler.ImportNotesHandler(w, r)
88+
default:
89+
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
90+
}
91+
})))
92+
8493
mux.Handle("/notes/", authMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
8594
switch r.Method {
8695
case http.MethodGet:

backend/internal/api/handler/handler.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,45 @@ func (h *Handler) CreateNodeHandler(w http.ResponseWriter, r *http.Request) {
8383
response.WriteResponse(w, http.StatusAccepted, fmt.Sprintf("%d", noteID))
8484
}
8585

86+
func (db *Handler) ImportNotesHandler(w http.ResponseWriter, r *http.Request) {
87+
db.Logger.Debug("ImportNotesHandler called")
88+
89+
httperr := validator.ImportNotesValidator(r)
90+
if httperr != nil {
91+
validator.WriteError(w, httperr)
92+
return
93+
}
94+
95+
clerkID, ok := auth.UserIDFromContext(r.Context())
96+
if !ok {
97+
http.Error(w, "unauthorized", http.StatusUnauthorized)
98+
return
99+
}
100+
101+
note, err := request.UpdateNoteparser(r)
102+
if err != nil {
103+
http.Error(w, "invalid request body", http.StatusBadRequest)
104+
return
105+
}
106+
107+
noteID, err := db.Service.CreateNote(r.Context(), clerkID)
108+
if err != nil {
109+
http.Error(w, err.Error(), http.StatusInternalServerError)
110+
return
111+
}
112+
113+
if err := db.Service.UpdateNodeById(r.Context(), note, noteID); err != nil {
114+
if deleteErr := db.Service.DelNoteById(r.Context(), noteID); deleteErr != nil {
115+
db.Logger.Error("failed to delete imported note after update failure", "note_id", noteID, "error", deleteErr)
116+
}
117+
118+
http.Error(w, err.Error(), http.StatusInternalServerError)
119+
return
120+
}
121+
122+
response.WriteResponse(w, http.StatusAccepted, fmt.Sprintf("%d", noteID))
123+
}
124+
86125
func (db *Handler) UpdateNoteHandler(w http.ResponseWriter, r *http.Request) {
87126
db.Logger.Debug("UpdateNoteHandler called")
88127

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

95134
note, err := request.UpdateNoteparser(r)
96-
check(err)
135+
if err != nil {
136+
http.Error(w, "invalid request body", http.StatusBadRequest)
137+
return
138+
}
97139

98-
db.Service.UpdateNodeById(r.Context(), note, id)
140+
if err := db.Service.UpdateNodeById(r.Context(), note, id); err != nil {
141+
http.Error(w, err.Error(), http.StatusInternalServerError)
142+
return
143+
}
99144
}
100145

101146
func (db *Handler) DelNoteHandler(w http.ResponseWriter, r *http.Request) {

backend/internal/api/handler/handler_test.go

Lines changed: 102 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,21 @@ import (
44
"backend/internal/api/request"
55
"backend/internal/db"
66
"context"
7+
"errors"
78
"io"
89
"log/slog"
910
"net/http"
1011
"net/http/httptest"
1112
"strings"
1213
"testing"
14+
15+
clerk "github.com/clerk/clerk-sdk-go/v2"
1316
"github.com/stretchr/testify/assert"
1417
"github.com/stretchr/testify/mock"
1518
)
1619

1720
type MockService struct {
1821
mock.Mock
19-
2022
}
2123

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

49+
func authenticatedRequest(req *http.Request, userID string) *http.Request {
50+
claims := &clerk.SessionClaims{}
51+
claims.Subject = userID
52+
ctx := clerk.ContextWithSessionClaims(req.Context(), claims)
53+
return req.WithContext(ctx)
54+
}
55+
4756
func TestNoteHandler(t *testing.T) {
4857
mockService := new(MockService)
4958

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

5463
handler := Handler{Service: mockService,
55-
Logger: slog.New(slog.NewTextHandler(io.Discard,nil)),
56-
}
64+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
65+
}
5766

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

7483
handler := Handler{Service: mockService,
75-
Logger: slog.New(slog.NewTextHandler(io.Discard,nil)),
76-
}
84+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
85+
}
7786

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

94103
handler := Handler{Service: mockService,
104+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
105+
}
106+
107+
body := `{"title":"test","content":{}}`
108+
req := httptest.NewRequest(http.MethodPatch, "/notes/1", strings.NewReader(body))
109+
w := httptest.NewRecorder()
110+
req.Header.Set("Content-Type", "application/json")
111+
112+
handler.UpdateNoteHandler(w, req)
95113

96-
Logger: slog.New(slog.NewTextHandler(io.Discard,nil)),
114+
assert.Equal(t, http.StatusOK, w.Code)
115+
mockService.AssertExpectations(t)
97116
}
98117

99-
body := `{"title":"test","content":{}}`
100-
req := httptest.NewRequest(http.MethodPatch, "/notes/1",
101-
strings.NewReader(body))
118+
func TestUpdateNoteHandlerRejectsInvalidJSON(t *testing.T) {
119+
mockService := new(MockService)
120+
handler := Handler{Service: mockService,
121+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
122+
}
102123

124+
req := httptest.NewRequest(http.MethodPatch, "/notes/1", strings.NewReader(`{"title":`))
125+
w := httptest.NewRecorder()
103126
req.Header.Set("Content-Type", "application/json")
127+
128+
handler.UpdateNoteHandler(w, req)
129+
130+
assert.Equal(t, http.StatusBadRequest, w.Code)
131+
mockService.AssertNotCalled(t, "UpdateNodeById", mock.Anything, mock.Anything, mock.Anything)
132+
}
133+
134+
func TestUpdateNoteHandlerReturnsInternalServerErrorOnUpdateFailure(t *testing.T) {
135+
mockService := new(MockService)
136+
mockService.
137+
On("UpdateNodeById", mock.Anything, mock.Anything, 1).
138+
Return(errors.New("update failed"))
139+
140+
handler := Handler{Service: mockService,
141+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
142+
}
143+
144+
body := `{"title":"test","content":{}}`
145+
req := httptest.NewRequest(http.MethodPatch, "/notes/1", strings.NewReader(body))
104146
w := httptest.NewRecorder()
147+
req.Header.Set("Content-Type", "application/json")
105148

106149
handler.UpdateNoteHandler(w, req)
107150

151+
assert.Equal(t, http.StatusInternalServerError, w.Code)
108152
mockService.AssertExpectations(t)
109153
}
110154

111-
func TestDelNoteHandler(t *testing.T) {
155+
func TestImportNotesHandlerRejectsInvalidJSONBeforeCreate(t *testing.T) {
112156
mockService := new(MockService)
157+
handler := Handler{Service: mockService,
158+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
159+
}
160+
161+
req := httptest.NewRequest(http.MethodPost, "/notes/imports", strings.NewReader(`{"title":`))
162+
req = authenticatedRequest(req, "user_123")
163+
req.Header.Set("Content-Type", "application/json")
164+
w := httptest.NewRecorder()
165+
166+
handler.ImportNotesHandler(w, req)
167+
168+
assert.Equal(t, http.StatusBadRequest, w.Code)
169+
mockService.AssertNotCalled(t, "CreateNote", mock.Anything, mock.Anything)
170+
mockService.AssertNotCalled(t, "UpdateNodeById", mock.Anything, mock.Anything, mock.Anything)
171+
mockService.AssertNotCalled(t, "DelNoteById", mock.Anything, mock.Anything)
172+
}
113173

174+
func TestImportNotesHandlerDeletesCreatedNoteOnUpdateFailure(t *testing.T) {
175+
mockService := new(MockService)
114176
mockService.
115-
On("DelNoteById", mock.Anything, 1).
177+
On("CreateNote", mock.Anything, "user_123").
178+
Return(42, nil)
179+
mockService.
180+
On("UpdateNodeById", mock.Anything, mock.Anything, 42).
181+
Return(errors.New("update failed"))
182+
mockService.
183+
On("DelNoteById", mock.Anything, 42).
116184
Return(nil)
117185

118-
119186
handler := Handler{Service: mockService,
120-
Logger: slog.New(slog.NewTextHandler(io.Discard,nil)),
187+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
188+
}
189+
190+
body := `{"title":"test","content":{}}`
191+
req := httptest.NewRequest(http.MethodPost, "/notes/imports", strings.NewReader(body))
192+
req = authenticatedRequest(req, "user_123")
193+
req.Header.Set("Content-Type", "application/json")
194+
w := httptest.NewRecorder()
195+
196+
handler.ImportNotesHandler(w, req)
197+
198+
assert.Equal(t, http.StatusInternalServerError, w.Code)
199+
mockService.AssertExpectations(t)
121200
}
122201

202+
func TestDelNoteHandler(t *testing.T) {
203+
mockService := new(MockService)
204+
205+
mockService.
206+
On("DelNoteById", mock.Anything, 1).
207+
Return(nil)
208+
209+
handler := Handler{Service: mockService,
210+
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
211+
}
123212

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

129218
mockService.AssertExpectations(t)
130219
}
131-
132-

backend/internal/api/validator/validator.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"log"
7+
"mime"
78
"net/http"
89
"strconv"
910
"strings"
@@ -36,6 +37,17 @@ func formatpath(path string) []string {
3637
return strings.Split(path, "/")
3738
}
3839

40+
func validateJSONContentType(r *http.Request) *httpError {
41+
contentType := r.Header.Get("Content-Type")
42+
mediaType, _, err := mime.ParseMediaType(contentType)
43+
if err != nil || mediaType != "application/json" {
44+
log.Printf("invalid content-type: %s", contentType)
45+
return &httpError{Status: http.StatusUnsupportedMediaType, Msg: "media not supported"}
46+
}
47+
48+
return nil
49+
}
50+
3951
func GetNoteValidator(r *http.Request) (int, *httpError) {
4052
if err := validatehttpmethod(r, http.MethodGet); err != nil {
4153
return 0, &httpError{Status: http.StatusBadRequest, Msg: "method is not supported"}
@@ -110,10 +122,8 @@ func UpdateNoteValidator(r *http.Request) (int, *httpError) {
110122
return 0, &httpError{Status: http.StatusMethodNotAllowed, Msg: "method not supported"}
111123
}
112124

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

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

136146
return id, nil
137147
}
148+
149+
func ImportNotesValidator(r *http.Request) *httpError {
150+
if err := validatehttpmethod(r, http.MethodPost); err != nil {
151+
return &httpError{Status: http.StatusMethodNotAllowed, Msg: "method not supported"}
152+
}
153+
154+
if err := validateJSONContentType(r); err != nil {
155+
return err
156+
}
157+
158+
parts := formatpath(r.URL.Path)
159+
if len(parts) != 2 || parts[0] != "notes" || parts[1] != "imports" {
160+
log.Printf("invalid path: %s", r.URL.Path)
161+
return &httpError{Status: http.StatusBadRequest, Msg: "path wrong"}
162+
}
163+
164+
return nil
165+
}

0 commit comments

Comments
 (0)