mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0 * test: stabilize water capture re-entry
This commit is contained in:
1 parent
b01822be48
commit
62eb431d66
372 files changed
+25178
-8561
No files matched your search
@@ -16,7 +16,7 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
import { __resetAllCachesForTests } from "@/lib/cache/server-cache";
|
||||
import { __resetAllCachesForTests, caches } from "@/lib/cache/server-cache";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
@@ -280,6 +280,73 @@ describe("GET /api/workouts — canonical dedup", () => {
|
||||
expect(body.data.workouts).toHaveLength(3);
|
||||
expect(body.data.meta.limit).toBe(3);
|
||||
});
|
||||
it("normalizes empty filters into the unfiltered cache key", async () => {
|
||||
vi.mocked(prisma.workout.findMany).mockResolvedValue([] as never);
|
||||
|
||||
for (const query of [
|
||||
"",
|
||||
"?since=&until=&sportType=",
|
||||
"?since=%20&until=%09&sportType=%20%20",
|
||||
]) {
|
||||
const res = await GET(makeRequest(query));
|
||||
expect(res.status).toBe(200);
|
||||
}
|
||||
|
||||
expect(prisma.workout.findMany).toHaveBeenCalledTimes(1);
|
||||
expect(caches.workouts.stats().size).toBe(1);
|
||||
});
|
||||
|
||||
it("canonicalizes equivalent timestamps and trimmed sport types before keying", async () => {
|
||||
vi.mocked(prisma.workout.findMany).mockResolvedValue([] as never);
|
||||
|
||||
const first = await GET(
|
||||
makeRequest(
|
||||
"?since=2026-01-01T00%3A00%3A00Z&until=2026-01-02T00%3A00%3A00Z&sportType=%20running%20",
|
||||
),
|
||||
);
|
||||
const second = await GET(
|
||||
makeRequest(
|
||||
"?since=2025-12-31T19%3A00%3A00-05%3A00&until=2026-01-01T19%3A00%3A00-05%3A00&sportType=running",
|
||||
),
|
||||
);
|
||||
|
||||
expect(first.status).toBe(200);
|
||||
expect(second.status).toBe(200);
|
||||
expect(prisma.workout.findMany).toHaveBeenCalledTimes(1);
|
||||
expect(prisma.workout.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
userId: "user-1",
|
||||
sportType: "running",
|
||||
startedAt: {
|
||||
gte: new Date("2026-01-01T00:00:00.000Z"),
|
||||
lte: new Date("2026-01-02T00:00:00.000Z"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects invalid filters without querying or minting cache keys", async () => {
|
||||
const adversarialQueries = [
|
||||
...Array.from(
|
||||
{ length: 128 },
|
||||
(_, index) => `?since=not-a-date-${index}`,
|
||||
),
|
||||
"?until=2026-02-30T00%3A00%3A00Z",
|
||||
"?sportType=not-a-sport",
|
||||
"?since=2026-01-02T00%3A00%3A00Z&until=2026-01-01T00%3A00%3A00Z",
|
||||
];
|
||||
for (const query of adversarialQueries) {
|
||||
const res = await GET(makeRequest(query));
|
||||
const body = await res.json();
|
||||
expect(res.status).toBe(422);
|
||||
expect(body.error).toBe("Validation failed");
|
||||
}
|
||||
|
||||
expect(prisma.workout.findMany).not.toHaveBeenCalled();
|
||||
expect(caches.workouts.stats().size).toBe(0);
|
||||
});
|
||||
|
||||
it("paginates across cluster boundaries without double-counting deduped rows", async () => {
|
||||
// Eight twin clusters spaced 1 hour apart: each cluster has an
|
||||
|
||||
@@ -59,8 +59,11 @@ import { NextRequest } from "next/server";
|
||||
|
||||
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { readWorkoutsListCached } from "@/lib/workouts/list-read";
|
||||
import { apiSuccess, returnAllZodIssues } from "@/lib/api-response";
|
||||
import {
|
||||
readWorkoutsListCached,
|
||||
workoutListFilterSchema,
|
||||
} from "@/lib/workouts/list-read";
|
||||
import { requireModuleEnabled } from "@/lib/modules/gate";
|
||||
|
||||
const DEFAULT_LIMIT = 50;
|
||||
@@ -90,16 +93,19 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
0,
|
||||
parseInt(searchParams.get("offset") ?? "0", 10) || 0,
|
||||
);
|
||||
const since = searchParams.get("since");
|
||||
const until = searchParams.get("until");
|
||||
const sportType = searchParams.get("sportType");
|
||||
const filterResult = workoutListFilterSchema.safeParse({
|
||||
since: searchParams.get("since"),
|
||||
until: searchParams.get("until"),
|
||||
sportType: searchParams.get("sportType"),
|
||||
});
|
||||
if (!filterResult.success) {
|
||||
return returnAllZodIssues(filterResult.error);
|
||||
}
|
||||
|
||||
const result = await readWorkoutsListCached(user.id, {
|
||||
limit,
|
||||
offset,
|
||||
since,
|
||||
until,
|
||||
sportType,
|
||||
...filterResult.data,
|
||||
});
|
||||
|
||||
return apiSuccess(result);
|
||||
|
||||
Reference in new issue
Block a user