fix(api): bind daily and synastry charts to stored profiles
Stop accepting client-supplied birth data on those paths, and cap session writes plus location lookups so a logged-in caller cannot farm compute. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -63,7 +63,13 @@ test("relationship intent selects domain-specific evidence instead of treating e
|
||||
assert.match(source, /亲友\/家庭/);
|
||||
assert.match(source, /其他关系/);
|
||||
assert.match(source, /relationshipType: SynastryRelationshipType/);
|
||||
assert.match(source, /body: JSON\.stringify\(\{ selfProfile: profile, partnerProfile: record\.profile, relationshipType \}\)/);
|
||||
assert.match(source, /body: JSON\.stringify\(\{ partnerChartProfileId: record\.id, relationshipType \}\)/);
|
||||
assert.doesNotMatch(source, /selfProfile: profile, partnerProfile: record\.profile/);
|
||||
assert.match(route, /partnerChartProfileId/);
|
||||
assert.match(route, /from\("profiles"\)/);
|
||||
assert.match(route, /from\("chart_profiles"\)/);
|
||||
assert.match(route, /eq\("role", "other"\)/);
|
||||
assert.match(route, /consumeUserRequestRateLimit\("synastry"/);
|
||||
assert.match(route, /relationshipType === "business"/);
|
||||
assert.match(route, /divisions: \["D2", "D10", "D11"\]/);
|
||||
assert.match(route, /"D10_Dasamsa"/);
|
||||
|
||||
@@ -77,6 +77,24 @@ test("owner or validation failures are not retried", async () => {
|
||||
assert.equal(attempts, 1);
|
||||
});
|
||||
|
||||
test("session writes reject oversized transcripts before they reach storage", () => {
|
||||
const oversized = chatSessionWriteSchema.safeParse({
|
||||
...values,
|
||||
messages: [{ role: "user", text: "字".repeat(16_001) }],
|
||||
});
|
||||
const tooMany = chatSessionWriteSchema.safeParse({
|
||||
...values,
|
||||
messages: Array.from({ length: 201 }, () => ({ role: "user" as const, text: "你好" })),
|
||||
});
|
||||
const tooMuchText = chatSessionWriteSchema.safeParse({
|
||||
...values,
|
||||
messages: Array.from({ length: 20 }, () => ({ role: "user" as const, text: "字".repeat(12_000) })),
|
||||
});
|
||||
assert.equal(oversized.success, false);
|
||||
assert.equal(tooMany.success, false);
|
||||
assert.equal(tooMuchText.success, false);
|
||||
});
|
||||
|
||||
test("session API owns create and update while answer UI keeps sync failures out of reply errors", () => {
|
||||
const page = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
const collectionRoute = readFileSync(new URL("../src/app/api/sessions/route.ts", import.meta.url), "utf8");
|
||||
@@ -87,6 +105,10 @@ test("session API owns create and update while answer UI keeps sync failures out
|
||||
assert.match(collectionRoute, /export async function POST/);
|
||||
assert.match(itemRoute, /export async function PATCH/);
|
||||
assert.match(itemRoute, /\.eq\("user_id", user\.id\)/);
|
||||
assert.match(collectionRoute, /readChatSessionJson/);
|
||||
assert.match(itemRoute, /readChatSessionJson/);
|
||||
assert.match(collectionRoute, /ChatSessionBodyTooLargeError/);
|
||||
assert.match(itemRoute, /ChatSessionBodyTooLargeError/);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -269,6 +269,11 @@ test("the homepage card is engine-backed first, with Agent polish off the reques
|
||||
|
||||
assert.match(route, /supabase\.auth\.getUser\(\)/);
|
||||
assert.match(route, /\{ status: "unauthenticated" \}, \{ status: 401 \}/);
|
||||
assert.match(route, /from\("profiles"\)/);
|
||||
assert.match(route, /globalBirthProfileFromAccountRow/);
|
||||
assert.match(route, /consumeUserRequestRateLimit\("dailyStarlanguage"/);
|
||||
assert.doesNotMatch(route, /body\?\.profile/);
|
||||
assert.doesNotMatch(route, /body\?\.today/);
|
||||
assert.match(route, /dailyStarlanguageCacheKey\(user\.id, JSON\.stringify\(payload\), today\)/);
|
||||
assert.match(route, /pending\(\)\.get\(key\)/);
|
||||
assert.match(route, /"\/api\/daily_guidance"/);
|
||||
@@ -289,6 +294,9 @@ test("the homepage card is engine-backed first, with Agent polish off the reques
|
||||
for (const source of [route, page]) {
|
||||
assert.doesNotMatch(source, /先收束,再推进|执行力比灵感更重要|适合观察资源流向/);
|
||||
}
|
||||
assert.match(page, /fetchDailyStarlanguage\(controller\.signal\)/);
|
||||
assert.match(page, /body: JSON\.stringify\(\{\}\)/);
|
||||
assert.doesNotMatch(page, /JSON\.stringify\(\{ profile, today \}\)/);
|
||||
assert.doesNotMatch(page, /buildDailyStarlanguageCard/);
|
||||
assert.match(route, /status: "unavailable"/);
|
||||
assert.match(page, /今天的星语还没写出来/);
|
||||
|
||||
@@ -129,3 +129,12 @@ test("birth time cannot be submitted without a birth date", () => {
|
||||
assert.equal(birthLocationSearchQuerySchema.safeParse({ q: "Paris", birthTime: "05:30" }).success, false);
|
||||
assert.equal(birthLocationSearchQuerySchema.safeParse({ q: "Paris", birthDate: "2021-02-29" }).success, false);
|
||||
});
|
||||
|
||||
test("location search and timezone routes bound logged-in callers before spending upstream quota", () => {
|
||||
const searchRoute = readFileSync(new URL("../src/app/api/locations/search/route.ts", import.meta.url), "utf8");
|
||||
const timezoneRoute = readFileSync(new URL("../src/app/api/locations/timezone/route.ts", import.meta.url), "utf8");
|
||||
assert.match(searchRoute, /consumeUserRequestRateLimit\("locationSearch"/);
|
||||
assert.match(timezoneRoute, /consumeUserRequestRateLimit\("locationTimezone"/);
|
||||
assert.match(searchRoute, /Retry-After/);
|
||||
assert.match(timezoneRoute, /Retry-After/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { consumeRequestRateLimit } from "../src/lib/request-rate-limit.ts";
|
||||
|
||||
test("a user is allowed up to the window limit and then must wait", () => {
|
||||
const store = new Map<string, number[]>();
|
||||
const first = consumeRequestRateLimit({
|
||||
key: "search:user-1",
|
||||
limit: 2,
|
||||
windowMs: 60_000,
|
||||
now: 1_000,
|
||||
store,
|
||||
});
|
||||
const second = consumeRequestRateLimit({
|
||||
key: "search:user-1",
|
||||
limit: 2,
|
||||
windowMs: 60_000,
|
||||
now: 2_000,
|
||||
store,
|
||||
});
|
||||
const blocked = consumeRequestRateLimit({
|
||||
key: "search:user-1",
|
||||
limit: 2,
|
||||
windowMs: 60_000,
|
||||
now: 3_000,
|
||||
store,
|
||||
});
|
||||
const otherUser = consumeRequestRateLimit({
|
||||
key: "search:user-2",
|
||||
limit: 2,
|
||||
windowMs: 60_000,
|
||||
now: 3_000,
|
||||
store,
|
||||
});
|
||||
const afterWindow = consumeRequestRateLimit({
|
||||
key: "search:user-1",
|
||||
limit: 2,
|
||||
windowMs: 60_000,
|
||||
now: 61_500,
|
||||
store,
|
||||
});
|
||||
|
||||
assert.deepEqual(first, { ok: true });
|
||||
assert.deepEqual(second, { ok: true });
|
||||
assert.deepEqual(blocked, { ok: false, retryAfterSeconds: 58 });
|
||||
assert.deepEqual(otherUser, { ok: true });
|
||||
assert.deepEqual(afterWindow, { ok: true });
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
globalBirthProfileFromAccountRow,
|
||||
globalBirthProfileFromStoredChart,
|
||||
} from "../src/lib/server-owned-birth-profile.ts";
|
||||
|
||||
test("accepted account rows use the server active minute, not a reported leftover", () => {
|
||||
const profile = globalBirthProfileFromAccountRow({
|
||||
name: "测试",
|
||||
birth_date: "1990-06-15",
|
||||
reported_birth_time: "08:00:00",
|
||||
active_birth_time: "09:15:00",
|
||||
birth_time_status: "accepted",
|
||||
country_code: "CN",
|
||||
province_code: "310000",
|
||||
city_code: "310100",
|
||||
latitude: 31.2,
|
||||
longitude: 121.5,
|
||||
timezone_offset: 8,
|
||||
timezone_id: "Asia/Shanghai",
|
||||
});
|
||||
|
||||
assert.equal(profile.date, "1990-06-15");
|
||||
assert.equal(profile.time, "09:15");
|
||||
assert.equal(profile.timezoneId, "Asia/Shanghai");
|
||||
assert.equal(profile.birthTimeStatus, "accepted");
|
||||
});
|
||||
|
||||
test("reported rows keep the declared clock until an active minute exists", () => {
|
||||
const profile = globalBirthProfileFromAccountRow({
|
||||
birth_date: new Date("1991-01-02T00:00:00.000Z"),
|
||||
reported_birth_time: "07:40",
|
||||
active_birth_time: null,
|
||||
birth_time_status: "reported",
|
||||
});
|
||||
|
||||
assert.equal(profile.date, "1991-01-02");
|
||||
assert.equal(profile.time, "07:40");
|
||||
});
|
||||
|
||||
test("stored other-chart JSON keeps the library camelCase shape", () => {
|
||||
const profile = globalBirthProfileFromStoredChart({
|
||||
name: "对方",
|
||||
date: "1988-03-04",
|
||||
time: "18:20",
|
||||
reportedTime: "18:20",
|
||||
countryCode: "CN",
|
||||
provinceCode: "110000",
|
||||
cityCode: "110100",
|
||||
latitude: 39.9,
|
||||
longitude: 116.4,
|
||||
timezoneOffset: 8,
|
||||
timezoneId: "Asia/Shanghai",
|
||||
});
|
||||
|
||||
assert.deepEqual(profile, {
|
||||
name: "对方",
|
||||
date: "1988-03-04",
|
||||
time: "18:20",
|
||||
countryCode: "CN",
|
||||
provinceCode: "110000",
|
||||
cityCode: "110100",
|
||||
districtCode: undefined,
|
||||
latitude: 39.9,
|
||||
longitude: 116.4,
|
||||
timezoneOffset: 8,
|
||||
timezoneId: "Asia/Shanghai",
|
||||
birthTimeStatus: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test("a stored chart without a clock cannot be used for synastry", () => {
|
||||
assert.equal(globalBirthProfileFromStoredChart({ date: "1990-01-01", name: "对方" }), null);
|
||||
assert.equal(globalBirthProfileFromStoredChart(null), null);
|
||||
});
|
||||
Reference in New Issue
Block a user