diff --git a/server/pkg/service/middleware_test.go b/server/pkg/service/middleware_test.go index e9c4603..59c92c5 100644 --- a/server/pkg/service/middleware_test.go +++ b/server/pkg/service/middleware_test.go @@ -232,9 +232,21 @@ func TestUserCtxMiddleware(t *testing.T) { urlID string statusCode int }{ - {"valid ID", validUserID.String(), http.StatusOK}, - {"invalid ID", invalidUserID, http.StatusNotFound}, - {"non existent ID", uuid.New().String(), http.StatusNotFound}, + { + "valid ID", + validUserID.String(), + http.StatusOK, + }, + { + "invalid ID", + invalidUserID, + http.StatusNotFound, + }, + { + "non existent ID", + uuid.New().String(), + http.StatusNotFound, + }, } for _, tc := range tests { @@ -257,6 +269,97 @@ func TestUserCtxMiddleware(t *testing.T) { } } +func TestNoteCtxMiddleware(t *testing.T) { + userID := uuid.New() + noteID := uuid.New() + + tests := []struct { + name string + noteID string + user any + mock func(*mockNoteStore) + statusCode int + }{ + { + "invalid note ID", + "invalid", + &userClaims{RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID.String(), + }}, + func(m *mockNoteStore) {}, + http.StatusNotFound, + }, + { + "unauthorized user", + noteID.String(), + nil, + func(m *mockNoteStore) {}, + http.StatusUnauthorized, + }, + { + "note not found", + noteID.String(), + &userClaims{RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID.String(), + }}, + func(m *mockNoteStore) { + m.GetNoteFunc = func(ctx context.Context, arg data.GetNoteParams) (data.Note, error) { + return data.Note{}, errors.New("not found") + } + }, + http.StatusNotFound, + }, + { + "success", + noteID.String(), + &userClaims{RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID.String(), + }}, + func(m *mockNoteStore) { + m.GetNoteFunc = func(ctx context.Context, arg data.GetNoteParams) (data.Note, error) { + assert.Equal(t, noteID, arg.ID) + assert.Equal(t, userID, arg.UserID) + return data.Note{ID: noteID}, nil + } + }, + http.StatusOK, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockStore := &mockNoteStore{} + tc.mock(mockStore) + + handler := noteCtx(mockStore)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, ok := r.Context().Value(noteCtxKey{}).(data.Note) + assert.True(t, ok) + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest("GET", fmt.Sprintf("/notes/%s", tc.noteID), nil) + + // Chi router context mocks ID passed in a URL parameter + rctx := chi.NewRouteContext() + rctx.URLParams.Add("id", tc.noteID) + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + if tc.user != nil { + req = req.WithContext(context.WithValue( + req.Context(), + userCtxKey{}, + tc.user, + )) + } + + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + + assert.Equal(t, tc.statusCode, w.Code) + }) + } +} + func generateTestToken(t *testing.T, secret, tokenType, userID string, isAdmin bool, opts ...func(*userClaims)) string { t.Helper() diff --git a/server/pkg/service/notes_test.go b/server/pkg/service/notes_test.go new file mode 100644 index 0000000..4fb8b57 --- /dev/null +++ b/server/pkg/service/notes_test.go @@ -0,0 +1,364 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "git.umbrella.haus/ae/notatest/pkg/data" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +type mockNoteStore struct { + CreateNoteFunc func(context.Context, uuid.UUID) (data.Note, error) + DeleteNoteFunc func(context.Context, data.DeleteNoteParams) error + GetNoteFunc func(context.Context, data.GetNoteParams) (data.Note, error) + ListNotesFunc func(context.Context, data.ListNotesParams) ([]data.Note, error) + CreateNoteVersionFunc func(context.Context, data.CreateNoteVersionParams) (data.NoteVersion, error) + FindDuplicateContentFunc func(context.Context, data.FindDuplicateContentParams) (bool, error) + GetNoteVersionFunc func(context.Context, data.GetNoteVersionParams) (data.NoteVersion, error) + GetNoteVersionsFunc func(context.Context, data.GetNoteVersionsParams) ([]data.NoteVersion, error) +} + +func (m *mockNoteStore) CreateNote(ctx context.Context, userID uuid.UUID) (data.Note, error) { + return m.CreateNoteFunc(ctx, userID) +} + +func (m *mockNoteStore) DeleteNote(ctx context.Context, arg data.DeleteNoteParams) error { + return m.DeleteNoteFunc(ctx, arg) +} + +func (m *mockNoteStore) GetNote(ctx context.Context, arg data.GetNoteParams) (data.Note, error) { + return m.GetNoteFunc(ctx, arg) +} + +func (m *mockNoteStore) ListNotes(ctx context.Context, arg data.ListNotesParams) ([]data.Note, error) { + return m.ListNotesFunc(ctx, arg) +} + +func (m *mockNoteStore) CreateNoteVersion(ctx context.Context, arg data.CreateNoteVersionParams) (data.NoteVersion, error) { + return m.CreateNoteVersionFunc(ctx, arg) +} + +func (m *mockNoteStore) FindDuplicateContent(ctx context.Context, arg data.FindDuplicateContentParams) (bool, error) { + return m.FindDuplicateContentFunc(ctx, arg) +} + +func (m *mockNoteStore) GetNoteVersion(ctx context.Context, arg data.GetNoteVersionParams) (data.NoteVersion, error) { + return m.GetNoteVersionFunc(ctx, arg) +} + +func (m *mockNoteStore) GetNoteVersions(ctx context.Context, arg data.GetNoteVersionsParams) ([]data.NoteVersion, error) { + return m.GetNoteVersionsFunc(ctx, arg) +} + +func TestNotes_CreateNote(t *testing.T) { + userID := uuid.New() + testNote := data.Note{ID: uuid.New(), UserID: userID} + + tests := []struct { + name string + mock func(*mockNoteStore) + statusCode int + }{ + { + "success", + func(m *mockNoteStore) { + m.CreateNoteFunc = func(_ context.Context, uid uuid.UUID) (data.Note, error) { + assert.Equal(t, userID, uid) + return testNote, nil + } + }, + http.StatusCreated, + }, + { + "database error", + func(m *mockNoteStore) { + m.CreateNoteFunc = func(context.Context, uuid.UUID) (data.Note, error) { + return data.Note{}, errors.New("db error") + } + }, + http.StatusInternalServerError, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockStore := &mockNoteStore{} + tc.mock(mockStore) + + rs := notesResource{Notes: mockStore} + req := httptest.NewRequest("POST", "/", nil) + req = req.WithContext(context.WithValue( + req.Context(), + userCtxKey{}, + &userClaims{RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID.String(), + }}, + )) + + w := httptest.NewRecorder() + rs.CreateNote(w, req) + + assert.Equal(t, tc.statusCode, w.Code) + if tc.statusCode == http.StatusCreated { + var note data.Note + json.Unmarshal(w.Body.Bytes(), ¬e) + assert.Equal(t, testNote.ID, note.ID) + } + }) + } +} + +func TestNotes_ListNotes(t *testing.T) { + userID := uuid.New() + notes := []data.Note{ + {ID: uuid.New(), UserID: userID}, + {ID: uuid.New(), UserID: userID}, + } + + tests := []struct { + name string + query string + mock func(*mockNoteStore) + statusCode int + }{ + { + "success", + "", + func(m *mockNoteStore) { + m.ListNotesFunc = func(_ context.Context, arg data.ListNotesParams) ([]data.Note, error) { + assert.Equal(t, userID, arg.UserID) + return notes, nil + } + }, + http.StatusOK, + }, + { + "with pagination", + "?limit=10&offset=20", + func(m *mockNoteStore) { + m.ListNotesFunc = func(_ context.Context, arg data.ListNotesParams) ([]data.Note, error) { + assert.EqualValues(t, 10, arg.Limit) + assert.EqualValues(t, 20, arg.Offset) + return notes, nil + } + }, + http.StatusOK, + }, + { + "database error", + "", + func(m *mockNoteStore) { + m.ListNotesFunc = func(context.Context, data.ListNotesParams) ([]data.Note, error) { + return nil, errors.New("db error") + } + }, + http.StatusInternalServerError, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockStore := &mockNoteStore{} + tc.mock(mockStore) + + rs := notesResource{Notes: mockStore} + req := httptest.NewRequest("GET", fmt.Sprintf("/%s", tc.query), nil) + req = req.WithContext(context.WithValue( + req.Context(), + userCtxKey{}, + &userClaims{RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID.String(), + }}, + )) + + w := httptest.NewRecorder() + rs.ListNotes(w, req) + + assert.Equal(t, tc.statusCode, w.Code) + }) + } +} + +func TestNotes_GetNote(t *testing.T) { + noteID := uuid.New() + userID := uuid.New() + validNote := data.Note{ID: noteID, UserID: userID} + + t.Run("success", func(t *testing.T) { + rs := notesResource{} + req := httptest.NewRequest("GET", "/", nil) + req = req.WithContext(context.WithValue( + req.Context(), + noteCtxKey{}, + validNote, + )) + + w := httptest.NewRecorder() + rs.GetNote(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + var note data.Note + json.Unmarshal(w.Body.Bytes(), ¬e) + assert.Equal(t, validNote.ID, note.ID) + }) + + t.Run("not found", func(t *testing.T) { + rs := notesResource{} + req := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + rs.GetNote(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func TestNotes_DeleteNote(t *testing.T) { + noteID := uuid.New() + userID := uuid.New() + validNote := data.Note{ID: noteID, UserID: userID} + + t.Run("success", func(t *testing.T) { + mockStore := &mockNoteStore{ + DeleteNoteFunc: func(_ context.Context, arg data.DeleteNoteParams) error { + assert.Equal(t, noteID, arg.ID) + assert.Equal(t, userID, arg.UserID) + return nil + }, + } + + rs := notesResource{Notes: mockStore} + req := httptest.NewRequest("DELETE", "/", nil) + req = req.WithContext(context.WithValue( + req.Context(), + noteCtxKey{}, + validNote, + )) + req = req.WithContext(context.WithValue( + req.Context(), + userCtxKey{}, + &userClaims{RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID.String(), + }}, + )) + + w := httptest.NewRecorder() + rs.DeleteNote(w, req) + + assert.Equal(t, http.StatusNoContent, w.Code) + }) + + t.Run("database error", func(t *testing.T) { + mockStore := &mockNoteStore{ + DeleteNoteFunc: func(context.Context, data.DeleteNoteParams) error { + return errors.New("db error") + }, + } + + rs := notesResource{Notes: mockStore} + req := httptest.NewRequest("DELETE", "/", nil) + req = req.WithContext(context.WithValue( + req.Context(), + noteCtxKey{}, + validNote, + )) + req = req.WithContext(context.WithValue( + req.Context(), + userCtxKey{}, + &userClaims{RegisteredClaims: jwt.RegisteredClaims{ + Subject: userID.String(), + }}, + )) + + w := httptest.NewRecorder() + rs.DeleteNote(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + }) +} + +func TestNotes_CreateNoteVersion(t *testing.T) { + noteID := uuid.New() + validRequest := `{"title": "Test", "content": "Content"}` + + tests := []struct { + name string + body string + mock func(*mockNoteStore) + statusCode int + }{ + { + "success", + validRequest, + func(m *mockNoteStore) { + m.FindDuplicateContentFunc = func(context.Context, data.FindDuplicateContentParams) (bool, error) { + return false, nil + } + m.CreateNoteVersionFunc = func(_ context.Context, arg data.CreateNoteVersionParams) (data.NoteVersion, error) { + assert.Equal(t, noteID, arg.NoteID) + return data.NoteVersion{}, nil + } + }, + http.StatusCreated, + }, + { + "duplicate content", + validRequest, + func(m *mockNoteStore) { + m.FindDuplicateContentFunc = func(context.Context, data.FindDuplicateContentParams) (bool, error) { + return true, nil + } + }, + http.StatusConflict, + }, + { + "invalid request", + "{invalid}", + func(m *mockNoteStore) {}, + http.StatusBadRequest, + }, + { + "database error", + validRequest, + func(m *mockNoteStore) { + m.FindDuplicateContentFunc = func(context.Context, data.FindDuplicateContentParams) (bool, error) { + return false, nil + } + m.CreateNoteVersionFunc = func(context.Context, data.CreateNoteVersionParams) (data.NoteVersion, error) { + return data.NoteVersion{}, errors.New("db error") + } + }, + http.StatusInternalServerError, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockStore := &mockNoteStore{} + tc.mock(mockStore) + + rs := notesResource{Notes: mockStore} + req := httptest.NewRequest("POST", "/", strings.NewReader(tc.body)) + req = req.WithContext(context.WithValue( + req.Context(), + noteCtxKey{}, + data.Note{ID: noteID}, + )) + + w := httptest.NewRecorder() + rs.CreateNoteVersion(w, req) + + assert.Equal(t, tc.statusCode, w.Code) + }) + } +} + +// TODO: add similar tests for `ListNoteVersions` and `GetNoteVersion`