chore(backend,web): bump feedback comment limit 500 -> 4096 chars

This commit is contained in:
Oliver Kieselbach
2026-05-19 13:14:09 +02:00
parent 5753a567a1
commit d697e02f04
5 changed files with 28 additions and 12 deletions
@@ -59,15 +59,19 @@ public sealed class SubmitOffboardingFeedbackFunctionTests
}
[Fact]
public async Task Submit_TrimsAndTruncatesComment_To500Chars()
public async Task Submit_TrimsAndTruncatesComment_To4096Chars()
{
// Departing tenants often have more context to share than the in-app 500-char
// limit allows; the offboarding endpoint accepts up to 4096 chars (still well
// under the Azure Tables 64 KB property cap). Anything beyond is silently
// truncated rather than rejected so the user's submit doesn't fail outright.
var h = Harness.WithInProgressMarker();
var longComment = new string('x', 600);
var longComment = new string('x', 5000);
var result = await h.Sut.ProcessSubmitAsync(TenantId, Upn, DisplayName, " " + longComment + " ");
Assert.Equal(SubmitOutcome.Ok, result.Outcome);
Assert.Equal(500, h.Repo.PersistedOffboardingFeedback!.Comment!.Length);
Assert.Equal(4096, h.Repo.PersistedOffboardingFeedback!.Comment!.Length);
Assert.DoesNotContain(" ", h.Repo.PersistedOffboardingFeedback.Comment!.Substring(0, 1));
}
@@ -32,7 +32,11 @@ namespace AutopilotMonitor.Functions.Functions.Admin;
/// </summary>
public class SubmitOffboardingFeedbackFunction
{
private const int MaxCommentLength = 500;
// Departing tenants often have more context to share than the in-app 500-char limit
// allows. Azure Table Storage caps a single string property at 64 KB, so 4096 chars
// (max ~16 KB UTF-8) sits well within both the table and the 16 KB request-body
// guard above.
private const int MaxCommentLength = 4096;
private readonly ILogger<SubmitOffboardingFeedbackFunction> _logger;
private readonly IOffboardingAuditRepository _offboardingRepo;
@@ -72,10 +76,12 @@ public class SubmitOffboardingFeedbackFunction
}
// Reject oversized bodies before parsing — defense against accidental clipboard-paste
// of huge logs into the textarea.
// of huge logs into the textarea. 64 KB is the Azure Table Storage single-property
// cap, which gives 4096 chars (max ~16 KB UTF-8) plus JSON envelope plenty of room
// while still bouncing pathological multi-MB pastes before we read them.
if (req.Headers.TryGetValues("Content-Length", out var clValues)
&& long.TryParse(clValues.FirstOrDefault(), out var contentLength)
&& contentLength > 16_384)
&& contentLength > 65_536)
{
return await BadRequest(req, "Request body too large");
}
@@ -159,10 +159,12 @@ namespace AutopilotMonitor.Functions.Functions.Feedback
return badRequest;
}
// Trim comment
// Trim comment. Aligned with the offboarding-feedback endpoint at 4096 chars
// so users have room for a substantive comment without hitting a tight limit;
// Azure Tables caps a single property at 64 KB so this stays well within.
var comment = body.Comment?.Trim();
if (comment?.Length > 500)
comment = comment.Substring(0, 500);
if (comment?.Length > 4096)
comment = comment.Substring(0, 4096);
// Upsert feedback record
await _feedbackRepo.SaveInAppFeedbackAsync(new FeedbackEntry
@@ -39,7 +39,7 @@ interface OffboardingSectionProps {
* (re-onboard → offboard) gets a fresh prompt each time. */
const FEEDBACK_SUBMITTED_KEY_PREFIX = "offboard-feedback-submitted:";
const FEEDBACK_MAX_CHARS = 500;
const FEEDBACK_MAX_CHARS = 4096;
function formatRemaining(totalSeconds: number): string {
if (totalSeconds <= 0) return "0 min";
@@ -7,6 +7,10 @@ import { authenticatedFetch } from "@/lib/authenticatedFetch";
type Phase = "loading" | "bubble" | "form" | "thankyou" | "hidden";
// Aligned with the offboarding-feedback textarea — gives users room for substantive
// comments without hitting an artificial 500-char limit. Backend cap matches.
const FEEDBACK_MAX_CHARS = 4096;
export default function FeedbackBubble() {
const { isAuthenticated, user, getAccessToken } = useAuth();
const [phase, setPhase] = useState<Phase>("loading");
@@ -163,13 +167,13 @@ export default function FeedbackBubble() {
<div>
<textarea
value={comment}
onChange={(e) => setComment(e.target.value.slice(0, 500))}
onChange={(e) => setComment(e.target.value.slice(0, FEEDBACK_MAX_CHARS))}
placeholder="Any feedback? (optional)"
rows={2}
className="w-full px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none placeholder-gray-400 dark:placeholder-gray-500"
/>
{comment.length > 0 && (
<p className="text-xs text-gray-400 text-right mt-0.5">{comment.length}/500</p>
<p className="text-xs text-gray-400 text-right mt-0.5">{comment.length}/{FEEDBACK_MAX_CHARS}</p>
)}
</div>