fix(measurements): show source badge on mobile card too

The Apple Health pink chip rendered only inside the desktop `<Table>`
cell. iOS users — the audience that actually consumes Apple Health
data — land on the mobile card branch and saw no source indicator at
all, risking duplicate hand-entries on top of synced rows. Add a
compact badge inline with the timestamp on the mobile card so the
distinction lands at every viewport. Component test asserts the
`measurement-source-badge` testid appears twice (desktop + mobile)
so the next layout refactor can't silently drop one branch.

Also extends the `POST /api/devices` unit test mocks for the
NotificationChannel auto-upsert added in the previous commit, plus a
new test that pins the same-user apnsToken collision path returning
201 idempotently (the cross-user path keeps its 409).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-11 02:16:18 +02:00
1 parent d5202e5dd8
commit a2dfc5e256
3 files changed
+60 -1

No files matched your search

@@ -9,9 +9,17 @@ vi.mock("@/lib/db", () => ({
create: vi.fn(),
update: vi.fn(),
},
notificationChannel: {
upsert: vi.fn(),
},
},
}));
vi.mock("@/lib/crypto", () => ({
encrypt: (s: string) => `enc(${s})`,
decrypt: (s: string) => s.replace(/^enc\(|\)$/g, ""),
}));
vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() }));
vi.mock("@/lib/auth/audit", () => ({
@@ -56,6 +64,9 @@ beforeEach(() => {
vi.mocked(prisma.device.findFirst).mockResolvedValue(null);
vi.mocked(prisma.device.create).mockResolvedValue({ id: "dev-1" } as never);
vi.mocked(prisma.device.update).mockResolvedValue({ id: "dev-1" } as never);
vi.mocked(prisma.notificationChannel.upsert).mockResolvedValue({
id: "ch-1",
} as never);
});
describe("POST /api/devices", () => {
@@ -182,6 +193,7 @@ describe("POST /api/devices", () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.device.findFirst).mockResolvedValue({
id: "other-dev",
userId: "other-user",
} as never);
const res = await POST(
req({
@@ -195,4 +207,27 @@ describe("POST /api/devices", () => {
expect(prisma.device.create).not.toHaveBeenCalled();
expect(prisma.device.update).not.toHaveBeenCalled();
});
it("falls through idempotently when the apnsToken already belongs to the same user", async () => {
// v1.4.23 W6 reconcile (HIGH 3): the cross-user-hijack guard used
// to filter `NOT: { userId }` so it skipped same-user collisions
// entirely, letting `findMany({ apnsToken })` fan a single push
// out to two Device rows. The guard now reads `userId` and only
// rejects cross-user collisions; same-user collisions fall
// through to the legacy-token upsert path.
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.device.findFirst).mockResolvedValue({
id: "self-dev",
userId: "user-1",
} as never);
const res = await POST(
req({
token: "abcd1234efgh5678",
bundleId: "io.healthlog.app",
apnsToken: "deadbeef".repeat(8),
apnsEnvironment: "sandbox",
}),
);
expect(res.status).toBe(201);
});
});