All checks were successful
Deploy / deploy (push) Successful in 2m34s
- Add PocketBase admin auth to notifications endpoint (was returning 0 users) - Store null instead of 100 for body battery when Garmin returns no data - Update decision engine to skip body battery rules when values are null - Dashboard and email already display "N/A" for null values Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
623 lines
19 KiB
TypeScript
623 lines
19 KiB
TypeScript
// ABOUTME: Unit tests for notifications cron endpoint.
|
|
// ABOUTME: Tests daily email notifications sent at user's preferred time.
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import type { DailyLog, User } from "@/types";
|
|
|
|
// Mock users and daily logs returned by PocketBase
|
|
let mockUsers: User[] = [];
|
|
let mockDailyLogs: DailyLog[] = [];
|
|
const mockPbUpdate = vi.fn().mockResolvedValue({ id: "log123" });
|
|
|
|
// Mock PocketBase with admin auth
|
|
const mockAuthWithPassword = vi.fn().mockResolvedValue({ id: "admin" });
|
|
vi.mock("@/lib/pocketbase", () => ({
|
|
createPocketBaseClient: vi.fn(() => ({
|
|
collection: vi.fn((name: string) => ({
|
|
getFullList: vi.fn(async () => {
|
|
if (name === "users") {
|
|
return mockUsers;
|
|
}
|
|
if (name === "dailyLogs") {
|
|
return mockDailyLogs;
|
|
}
|
|
return [];
|
|
}),
|
|
update: mockPbUpdate,
|
|
authWithPassword: (email: string, password: string) =>
|
|
mockAuthWithPassword(email, password),
|
|
})),
|
|
})),
|
|
}));
|
|
|
|
// Mock logger
|
|
vi.mock("@/lib/logger", () => ({
|
|
logger: {
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
// Mock email sending
|
|
const mockSendDailyEmail = vi.fn().mockResolvedValue(undefined);
|
|
const mockSendTokenExpirationWarning = vi.fn().mockResolvedValue(undefined);
|
|
const mockSendPeriodConfirmationEmail = vi.fn().mockResolvedValue(undefined);
|
|
|
|
vi.mock("@/lib/email", () => ({
|
|
sendDailyEmail: (data: unknown) => mockSendDailyEmail(data),
|
|
sendTokenExpirationWarning: (...args: unknown[]) =>
|
|
mockSendTokenExpirationWarning(...args),
|
|
sendPeriodConfirmationEmail: (...args: unknown[]) =>
|
|
mockSendPeriodConfirmationEmail(...args),
|
|
}));
|
|
|
|
import { POST } from "./route";
|
|
|
|
describe("POST /api/cron/notifications", () => {
|
|
const validSecret = "test-cron-secret";
|
|
|
|
// Helper to create a mock user
|
|
function createMockUser(overrides: Partial<User> = {}): User {
|
|
return {
|
|
id: "user123",
|
|
email: "test@example.com",
|
|
garminConnected: true,
|
|
garminOauth1Token: "encrypted:oauth1-token",
|
|
garminOauth2Token: "encrypted:oauth2-token",
|
|
garminTokenExpiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
|
|
garminRefreshTokenExpiresAt: null,
|
|
calendarToken: "cal-token",
|
|
lastPeriodDate: new Date("2025-01-01"),
|
|
cycleLength: 28,
|
|
notificationTime: "07:00",
|
|
timezone: "UTC",
|
|
activeOverrides: [],
|
|
intensityGoalMenstrual: 75,
|
|
intensityGoalFollicular: 150,
|
|
intensityGoalOvulation: 100,
|
|
intensityGoalEarlyLuteal: 120,
|
|
intensityGoalLateLuteal: 50,
|
|
created: new Date("2024-01-01"),
|
|
updated: new Date("2025-01-10"),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// Helper to create a mock daily log
|
|
function createMockDailyLog(overrides: Partial<DailyLog> = {}): DailyLog {
|
|
return {
|
|
id: "log123",
|
|
user: "user123",
|
|
date: new Date(),
|
|
cycleDay: 5,
|
|
phase: "FOLLICULAR",
|
|
bodyBatteryCurrent: 85,
|
|
bodyBatteryYesterdayLow: 50,
|
|
hrvStatus: "Balanced",
|
|
weekIntensityMinutes: 45,
|
|
phaseLimit: 120,
|
|
remainingMinutes: 75,
|
|
trainingDecision: "TRAIN",
|
|
decisionReason: "OK to train - follow phase plan",
|
|
notificationSentAt: null,
|
|
created: new Date(),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// Helper to create mock request with optional auth header
|
|
function createMockRequest(authHeader?: string): Request {
|
|
const headers = new Headers();
|
|
if (authHeader) {
|
|
headers.set("authorization", authHeader);
|
|
}
|
|
return {
|
|
headers,
|
|
} as unknown as Request;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockUsers = [];
|
|
mockDailyLogs = [];
|
|
process.env.CRON_SECRET = validSecret;
|
|
process.env.POCKETBASE_ADMIN_EMAIL = "admin@example.com";
|
|
process.env.POCKETBASE_ADMIN_PASSWORD = "admin-password";
|
|
mockAuthWithPassword.mockResolvedValue({ id: "admin" });
|
|
// Mock current time to 07:00 UTC
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2025-01-15T07:00:00Z"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
describe("Authentication", () => {
|
|
it("returns 401 when authorization header is missing", async () => {
|
|
const response = await POST(createMockRequest());
|
|
|
|
expect(response.status).toBe(401);
|
|
const body = await response.json();
|
|
expect(body.error).toBe("Unauthorized");
|
|
});
|
|
|
|
it("returns 401 when secret is incorrect", async () => {
|
|
const response = await POST(createMockRequest("Bearer wrong-secret"));
|
|
|
|
expect(response.status).toBe(401);
|
|
const body = await response.json();
|
|
expect(body.error).toBe("Unauthorized");
|
|
});
|
|
|
|
it("returns 401 when CRON_SECRET env var is not set", async () => {
|
|
process.env.CRON_SECRET = "";
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(401);
|
|
});
|
|
|
|
it("returns 500 when POCKETBASE_ADMIN_EMAIL is not set", async () => {
|
|
process.env.POCKETBASE_ADMIN_EMAIL = "";
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(500);
|
|
const body = await response.json();
|
|
expect(body.error).toBe("Server misconfiguration");
|
|
});
|
|
|
|
it("returns 500 when POCKETBASE_ADMIN_PASSWORD is not set", async () => {
|
|
process.env.POCKETBASE_ADMIN_PASSWORD = "";
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(500);
|
|
const body = await response.json();
|
|
expect(body.error).toBe("Server misconfiguration");
|
|
});
|
|
|
|
it("returns 500 when PocketBase admin auth fails", async () => {
|
|
mockAuthWithPassword.mockRejectedValueOnce(new Error("Auth failed"));
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(500);
|
|
const body = await response.json();
|
|
expect(body.error).toBe("Database authentication failed");
|
|
});
|
|
});
|
|
|
|
describe("User time matching", () => {
|
|
it("sends notification when current hour matches user notificationTime in UTC", async () => {
|
|
// Current time is 07:00 UTC, user wants notifications at 07:00 UTC
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not send notification when hour does not match", async () => {
|
|
// Current time is 07:00 UTC, user wants notifications at 08:00 UTC
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "08:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("handles timezone conversion correctly", async () => {
|
|
// Current time is 07:00 UTC = 02:00 America/New_York (EST is UTC-5)
|
|
mockUsers = [
|
|
createMockUser({
|
|
notificationTime: "02:00",
|
|
timezone: "America/New_York",
|
|
}),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("skips users with non-matching timezone hours", async () => {
|
|
// Current time is 07:00 UTC = 02:00 EST, user wants 07:00 EST (which is 12:00 UTC)
|
|
mockUsers = [
|
|
createMockUser({
|
|
notificationTime: "07:00",
|
|
timezone: "America/New_York",
|
|
}),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Quarter-hour time matching", () => {
|
|
it("sends notification at exact 15-minute slot (07:15)", async () => {
|
|
// Current time is 07:15 UTC
|
|
vi.setSystemTime(new Date("2025-01-15T07:15:00Z"));
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:15", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("rounds down notification time to nearest 15-minute slot (07:10 -> 07:00)", async () => {
|
|
// Current time is 07:00 UTC
|
|
vi.setSystemTime(new Date("2025-01-15T07:00:00Z"));
|
|
// User set 07:10, which rounds down to 07:00 slot
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:10", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("rounds down notification time (07:29 -> 07:15)", async () => {
|
|
// Current time is 07:15 UTC
|
|
vi.setSystemTime(new Date("2025-01-15T07:15:00Z"));
|
|
// User set 07:29, which rounds down to 07:15 slot
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:29", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not send notification when minute slot does not match", async () => {
|
|
// Current time is 07:00 UTC
|
|
vi.setSystemTime(new Date("2025-01-15T07:00:00Z"));
|
|
// User wants 07:15, but current slot is 07:00
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:15", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("handles 30-minute slot correctly", async () => {
|
|
// Current time is 07:30 UTC
|
|
vi.setSystemTime(new Date("2025-01-15T07:30:00Z"));
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:30", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("handles 45-minute slot correctly", async () => {
|
|
// Current time is 07:45 UTC
|
|
vi.setSystemTime(new Date("2025-01-15T07:45:00Z"));
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:45", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).toHaveBeenCalled();
|
|
});
|
|
|
|
it("handles timezone with 15-minute matching", async () => {
|
|
// Current time is 07:15 UTC = 02:15 America/New_York (EST is UTC-5)
|
|
vi.setSystemTime(new Date("2025-01-15T07:15:00Z"));
|
|
mockUsers = [
|
|
createMockUser({
|
|
notificationTime: "02:15",
|
|
timezone: "America/New_York",
|
|
}),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("DailyLog handling", () => {
|
|
it("does not send notification if no DailyLog exists for today", async () => {
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = []; // No daily log
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).not.toHaveBeenCalled();
|
|
const body = await response.json();
|
|
expect(body.skippedNoLog).toBe(1);
|
|
});
|
|
|
|
it("does not send notification if already sent today", async () => {
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog({ notificationSentAt: new Date() })];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockSendDailyEmail).not.toHaveBeenCalled();
|
|
const body = await response.json();
|
|
expect(body.skippedAlreadySent).toBe(1);
|
|
});
|
|
|
|
it("updates notificationSentAt after sending email", async () => {
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog({ id: "log456" })];
|
|
|
|
await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(mockPbUpdate).toHaveBeenCalledWith("log456", {
|
|
notificationSentAt: expect.any(String),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Email content", () => {
|
|
it("sends email with correct user email address", async () => {
|
|
mockUsers = [
|
|
createMockUser({
|
|
email: "recipient@example.com",
|
|
notificationTime: "07:00",
|
|
timezone: "UTC",
|
|
}),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog()];
|
|
|
|
await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(mockSendDailyEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
to: "recipient@example.com",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("includes cycle day and phase from DailyLog", async () => {
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [
|
|
createMockDailyLog({ cycleDay: 10, phase: "FOLLICULAR" }),
|
|
];
|
|
|
|
await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(mockSendDailyEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
cycleDay: 10,
|
|
phase: "FOLLICULAR",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("includes biometric data from DailyLog", async () => {
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [
|
|
createMockDailyLog({
|
|
bodyBatteryCurrent: 90,
|
|
bodyBatteryYesterdayLow: 45,
|
|
hrvStatus: "Balanced",
|
|
weekIntensityMinutes: 60,
|
|
phaseLimit: 120,
|
|
remainingMinutes: 60,
|
|
}),
|
|
];
|
|
|
|
await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(mockSendDailyEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
bodyBatteryCurrent: 90,
|
|
bodyBatteryYesterdayLow: 45,
|
|
hrvStatus: "Balanced",
|
|
weekIntensity: 60,
|
|
phaseLimit: 120,
|
|
remainingMinutes: 60,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("includes training decision from DailyLog", async () => {
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [
|
|
createMockDailyLog({
|
|
trainingDecision: "TRAIN",
|
|
decisionReason: "OK to train - follow phase plan",
|
|
}),
|
|
];
|
|
|
|
await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(mockSendDailyEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
decision: expect.objectContaining({
|
|
status: "TRAIN",
|
|
reason: "OK to train - follow phase plan",
|
|
}),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("includes nutrition guidance based on cycle day", async () => {
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [createMockDailyLog({ cycleDay: 10 })]; // Day 10 = follicular, flax+pumpkin seeds
|
|
|
|
await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(mockSendDailyEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
seeds: expect.stringContaining("Flax"),
|
|
carbRange: expect.any(String),
|
|
ketoGuidance: expect.any(String),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Error handling", () => {
|
|
it("continues processing other users when email sending fails", async () => {
|
|
mockUsers = [
|
|
createMockUser({
|
|
id: "user1",
|
|
email: "user1@example.com",
|
|
notificationTime: "07:00",
|
|
timezone: "UTC",
|
|
}),
|
|
createMockUser({
|
|
id: "user2",
|
|
email: "user2@example.com",
|
|
notificationTime: "07:00",
|
|
timezone: "UTC",
|
|
}),
|
|
];
|
|
mockDailyLogs = [
|
|
createMockDailyLog({ id: "log1", user: "user1" }),
|
|
createMockDailyLog({ id: "log2", user: "user2" }),
|
|
];
|
|
// First email fails, second succeeds
|
|
mockSendDailyEmail
|
|
.mockRejectedValueOnce(new Error("Email error"))
|
|
.mockResolvedValueOnce(undefined);
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
const body = await response.json();
|
|
expect(body.errors).toBe(1);
|
|
expect(body.notificationsSent).toBe(1);
|
|
});
|
|
|
|
it("handles null body battery values", async () => {
|
|
mockUsers = [
|
|
createMockUser({ notificationTime: "07:00", timezone: "UTC" }),
|
|
];
|
|
mockDailyLogs = [
|
|
createMockDailyLog({
|
|
bodyBatteryCurrent: null,
|
|
bodyBatteryYesterdayLow: null,
|
|
}),
|
|
];
|
|
|
|
await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(mockSendDailyEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
bodyBatteryCurrent: null,
|
|
bodyBatteryYesterdayLow: null,
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Response format", () => {
|
|
it("returns success with zero notifications when no users match", async () => {
|
|
mockUsers = [];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
const body = await response.json();
|
|
expect(body).toMatchObject({
|
|
success: true,
|
|
notificationsSent: 0,
|
|
errors: 0,
|
|
});
|
|
});
|
|
|
|
it("returns summary with counts", async () => {
|
|
mockUsers = [
|
|
createMockUser({
|
|
id: "user1",
|
|
notificationTime: "07:00",
|
|
timezone: "UTC",
|
|
}),
|
|
createMockUser({
|
|
id: "user2",
|
|
notificationTime: "07:00",
|
|
timezone: "UTC",
|
|
}),
|
|
];
|
|
mockDailyLogs = [
|
|
createMockDailyLog({ id: "log1", user: "user1" }),
|
|
createMockDailyLog({ id: "log2", user: "user2" }),
|
|
];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
expect(response.status).toBe(200);
|
|
const body = await response.json();
|
|
expect(body).toMatchObject({
|
|
success: true,
|
|
notificationsSent: 2,
|
|
errors: 0,
|
|
skippedNoLog: 0,
|
|
skippedAlreadySent: 0,
|
|
skippedWrongTime: 0,
|
|
});
|
|
});
|
|
|
|
it("includes timestamp in response", async () => {
|
|
mockUsers = [];
|
|
|
|
const response = await POST(createMockRequest(`Bearer ${validSecret}`));
|
|
|
|
const body = await response.json();
|
|
expect(body.timestamp).toBeDefined();
|
|
expect(new Date(body.timestamp)).toBeInstanceOf(Date);
|
|
});
|
|
});
|
|
});
|