feat(notes): raise annotation text limit from 10,000 to 100,000 chars

The 10k validator cap rejected legitimate long-form notes — a
scholarly reading note with quoted passages and footnotes lands
around 10.1k chars and failed at save with the raw validator error.

Raise the cap to 100,000 on all six annotation text fields in the
media handler request structs:

- CreateMediaNoteRequest / UpdateMediaNoteRequest Content
- CreateMediaHighlightRequest / UpdateMediaHighlightRequest NoteText
- CreateMediaBookmarkRequest / UpdateMediaBookmarkRequest Notes

No other layer changes: the media_notes / media_highlights columns
are unbounded TEXT, and the KOReader + websocket sync paths never
had a length cap, so the REST API now matches the rest of the
system instead of being the strictest gate.

New integration test pins the boundary: 50k and 100k-char notes
return 201, 100,001 chars returns 400.
This commit is contained in:
John O'Keefe
2026-10-02 21:31:03 -04:00
parent 29ac420876
commit 1205526422
2 changed files with 60 additions and 6 deletions
@@ -0,0 +1,54 @@
package main
import (
"bytes"
"encoding/json"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestMediaNoteLengthLimits verifies the note content validator cap
// (internal/handlers/media.go): content up to 100,000 chars is accepted,
// longer content is rejected with 400.
func TestMediaNoteLengthLimits(t *testing.T) {
setup := setupTestServer(t)
token := setup.Token
mediaItemID := createTestMediaItemID(t, setup)
postNote := func(content string) int {
payload := map[string]interface{}{
"content": content,
"position": "page:1",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items/"+mediaItemID+"/notes", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
defer func(Body io.ReadCloser) {
_ = Body.Close()
}(resp.Body)
return resp.StatusCode
}
t.Run("50k-char note accepted", func(t *testing.T) {
assert.Equal(t, http.StatusCreated, postNote(strings.Repeat("a", 50000)))
})
t.Run("100k-char note accepted at boundary", func(t *testing.T) {
assert.Equal(t, http.StatusCreated, postNote(strings.Repeat("b", 100000)))
})
t.Run("over-limit note rejected", func(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, postNote(strings.Repeat("c", 100001)))
})
}
+6 -6
View File
@@ -102,13 +102,13 @@ type UpdateMediaItemRequest struct {
// CreateMediaNoteRequest represents the request for creating a media note // CreateMediaNoteRequest represents the request for creating a media note
type CreateMediaNoteRequest struct { type CreateMediaNoteRequest struct {
Content string `json:"content" validate:"required,min=1,max=10000"` Content string `json:"content" validate:"required,min=1,max=100000"`
Position string `json:"position"` Position string `json:"position"`
} }
// UpdateMediaNoteRequest represents the request for updating a media note // UpdateMediaNoteRequest represents the request for updating a media note
type UpdateMediaNoteRequest struct { type UpdateMediaNoteRequest struct {
Content string `json:"content" validate:"required,min=1,max=10000"` Content string `json:"content" validate:"required,min=1,max=100000"`
Position string `json:"position"` Position string `json:"position"`
} }
@@ -120,7 +120,7 @@ type CreateMediaHighlightRequest struct {
EpubcfiStart string `json:"epubcfi_start" validate:"max=2000"` EpubcfiStart string `json:"epubcfi_start" validate:"max=2000"`
EpubcfiEnd string `json:"epubcfi_end" validate:"max=2000"` EpubcfiEnd string `json:"epubcfi_end" validate:"max=2000"`
Color string `json:"color" validate:"omitempty,len=7"` Color string `json:"color" validate:"omitempty,len=7"`
NoteText string `json:"note_text" validate:"max=10000"` NoteText string `json:"note_text" validate:"max=100000"`
NoteID string `json:"note_id"` NoteID string `json:"note_id"`
PercentageStart float64 `json:"percentage_start"` PercentageStart float64 `json:"percentage_start"`
PercentageEnd float64 `json:"percentage_end"` PercentageEnd float64 `json:"percentage_end"`
@@ -135,7 +135,7 @@ type UpdateMediaHighlightRequest struct {
EpubcfiStart string `json:"epubcfi_start" validate:"max=2000"` EpubcfiStart string `json:"epubcfi_start" validate:"max=2000"`
EpubcfiEnd string `json:"epubcfi_end" validate:"max=2000"` EpubcfiEnd string `json:"epubcfi_end" validate:"max=2000"`
Color string `json:"color" validate:"omitempty,len=7"` Color string `json:"color" validate:"omitempty,len=7"`
NoteText string `json:"note_text" validate:"max=10000"` NoteText string `json:"note_text" validate:"max=100000"`
NoteID string `json:"note_id"` NoteID string `json:"note_id"`
PercentageStart float64 `json:"percentage_start"` PercentageStart float64 `json:"percentage_start"`
PercentageEnd float64 `json:"percentage_end"` PercentageEnd float64 `json:"percentage_end"`
@@ -146,7 +146,7 @@ type UpdateMediaHighlightRequest struct {
type CreateMediaBookmarkRequest struct { type CreateMediaBookmarkRequest struct {
Title string `json:"title" validate:"required,min=1,max=255"` Title string `json:"title" validate:"required,min=1,max=255"`
Position string `json:"position" validate:"max=100"` Position string `json:"position" validate:"max=100"`
Notes string `json:"notes" validate:"max=10000"` Notes string `json:"notes" validate:"max=100000"`
CfiPosition string `json:"cfi_position" validate:"max=255"` CfiPosition string `json:"cfi_position" validate:"max=255"`
PageNumber int32 `json:"page_number"` PageNumber int32 `json:"page_number"`
ChapterNumber int32 `json:"chapter_number"` ChapterNumber int32 `json:"chapter_number"`
@@ -160,7 +160,7 @@ type CreateMediaBookmarkRequest struct {
// UpdateMediaBookmarkRequest represents the request for updating a media bookmark // UpdateMediaBookmarkRequest represents the request for updating a media bookmark
type UpdateMediaBookmarkRequest struct { type UpdateMediaBookmarkRequest struct {
Title string `json:"title" validate:"required,min=1,max=255"` Title string `json:"title" validate:"required,min=1,max=255"`
Notes string `json:"notes" validate:"max=10000"` Notes string `json:"notes" validate:"max=100000"`
Position string `json:"position" validate:"max=100"` Position string `json:"position" validate:"max=100"`
} }