"Jämna varv" and "Stegträning" previously only lived on the admin Training page. A driver's own personal mobile profile link now offers the same self-service configure/start/stop flow already used for the 5-minute practice timer there, with live progress computed server-side from the driver's own share token so raw lap rows and driver IDs never reach the client. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
301 lines
16 KiB
JavaScript
301 lines
16 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createRequire } from "node:module";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const {
|
|
buildPublicTrainingProfile,
|
|
findPublicTrainingShare,
|
|
hasPublicTrainingShare,
|
|
getPublicBestDurationResult,
|
|
normalizePublicTrainingSince,
|
|
normalizePublicTrainingWindow,
|
|
splitPublicTrainingStints,
|
|
addPublicTrainingMarker,
|
|
savePublicEvenLapsConfig,
|
|
savePublicStepTrainingConfig,
|
|
} = require("../server/public_training_profile.js");
|
|
|
|
const DRIVER_ONE_TOKEN = "driver-one-token-12345678901234567890";
|
|
const DRIVER_TWO_TOKEN = "driver-two-token-12345678901234567890";
|
|
const DISABLED_TOKEN = "disabled-token-123456789012345678901";
|
|
|
|
function buildState(now = Date.now()) {
|
|
return {
|
|
drivers: [
|
|
{ id: "driver_1", profileId: "profile_1", name: "Driver One", brand: "Associated 2WD", classId: "class_1", transponder: "secret-transponder-one" },
|
|
{ id: "driver_1_4wd", profileId: "profile_1", name: "Driver One", brand: "Associated 4WD", classId: "class_2", transponder: "secret-transponder-four" },
|
|
{ id: "driver_2", profileId: "profile_2", name: "Driver Two", brand: "Xray", classId: "class_1", transponder: "secret-transponder-two" },
|
|
],
|
|
classes: [
|
|
{ id: "class_1", name: "2WD" },
|
|
{ id: "class_2", name: "4WD" },
|
|
],
|
|
settings: {
|
|
clubName: "Test Club",
|
|
clubTagline: "Practice",
|
|
logoDataUrl: "javascript:alert(1)",
|
|
wsUrl: "ws://secret-decoder:9000",
|
|
},
|
|
training: {
|
|
shares: [
|
|
{ driverId: "driver_1", token: DRIVER_ONE_TOKEN, enabled: true },
|
|
{ driverId: "driver_2", token: DRIVER_TWO_TOKEN, enabled: true },
|
|
{ driverId: "driver_1", token: DISABLED_TOKEN, enabled: false },
|
|
],
|
|
goals: [
|
|
{ driverId: "driver_1", title: "Bronze", reward: "Badge", targetLaps: 5 },
|
|
{ driverId: "driver_2", title: "Other goal", reward: "Other reward", targetLaps: 10 },
|
|
],
|
|
ledger: [
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 25 * 60000, lapMs: 19000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 120000, lapMs: 18000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 12000, lapMs: 16500 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 4000, lapMs: 16000 },
|
|
{ driverId: "driver_1", sessionId: "free", timestamp: now - 2000, lapMs: 500 },
|
|
{ driverId: "driver_1_4wd", sessionId: "free", timestamp: now - 11000, lapMs: 14500 },
|
|
{ driverId: "driver_1_4wd", sessionId: "free", timestamp: now - 5000, lapMs: 14000 },
|
|
{ driverId: "driver_2", sessionId: "free", timestamp: now - 3000, lapMs: 12000 },
|
|
],
|
|
},
|
|
resultsBySession: {
|
|
free: {
|
|
splits: [
|
|
{ driverId: "driver_1", splitMs: 7200 },
|
|
{ driverId: "driver_1", splitMs: 7000 },
|
|
{ driverId: "driver_1_4wd", splitMs: 6400 },
|
|
{ driverId: "driver_2", splitMs: 6100 },
|
|
],
|
|
pitStops: [
|
|
{ driverId: "driver_1", pitStopMs: 12500, transponder: "secret-transponder-one" },
|
|
{ driverId: "driver_1_4wd", pitStopMs: 11000, transponder: "secret-transponder-four" },
|
|
{ driverId: "driver_2", pitStopMs: 9500, transponder: "secret-transponder-two" },
|
|
],
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
test("public share lookup rejects short, disabled, and unknown tokens", () => {
|
|
const state = buildState();
|
|
assert.equal(findPublicTrainingShare(state, "short"), null);
|
|
assert.equal(findPublicTrainingShare(state, DISABLED_TOKEN), null);
|
|
assert.equal(hasPublicTrainingShare(state, "unknown-token-123456789012345678"), false);
|
|
assert.equal(hasPublicTrainingShare(state, DRIVER_ONE_TOKEN), true);
|
|
});
|
|
|
|
test("public mobile marker is restricted to the shared profile and selected class", () => {
|
|
const state = buildState();
|
|
const marker = addPublicTrainingMarker(state, DRIVER_ONE_TOKEN, { classId: "class_2", type: "tires", note: " New rear tires " }, 123456);
|
|
|
|
assert.deepEqual({ driverId: marker.driverId, classId: marker.classId, type: marker.type, note: marker.note, timestamp: marker.timestamp }, { driverId: "driver_1_4wd", classId: "class_2", type: "tires", note: "New rear tires", timestamp: 123456 });
|
|
assert.equal(state.training.markers.length, 1);
|
|
assert.equal(addPublicTrainingMarker(state, DRIVER_TWO_TOKEN, { classId: "class_2" }, 123457), null);
|
|
assert.equal(addPublicTrainingMarker(state, DISABLED_TOKEN, { classId: "class_1" }, 123458), null);
|
|
});
|
|
|
|
test("a driver can save their own even-laps and step-training exercise from their mobile link", () => {
|
|
const state = buildState();
|
|
const evenLaps = savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 14000, toleranceMs: 300, lapCount: 6 });
|
|
assert.deepEqual({ driverId: evenLaps.driverId, classId: evenLaps.classId, targetMs: evenLaps.targetMs, toleranceMs: evenLaps.toleranceMs, lapCount: evenLaps.lapCount }, { driverId: "driver_1_4wd", classId: "class_2", targetMs: 14000, toleranceMs: 300, lapCount: 6 });
|
|
assert.equal(state.training.evenLaps.length, 1);
|
|
|
|
// Saving again for the same driver+class updates in place rather than adding a duplicate.
|
|
savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 13500, toleranceMs: 250, lapCount: 5 });
|
|
assert.equal(state.training.evenLaps.length, 1);
|
|
assert.equal(state.training.evenLaps[0].targetMs, 13500);
|
|
|
|
// A different driver's token, or a class the driver isn't in, must not be able to write it.
|
|
assert.equal(savePublicEvenLapsConfig(state, DRIVER_TWO_TOKEN, { classId: "class_2", targetMs: 10000, toleranceMs: 300, lapCount: 5 }), null);
|
|
assert.equal(savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "unknown_class", targetMs: 10000, toleranceMs: 300, lapCount: 5 }), null);
|
|
assert.equal(savePublicEvenLapsConfig(state, DRIVER_ONE_TOKEN, { classId: "class_2", targetMs: 0, toleranceMs: 300, lapCount: 5 }), null, "a non-positive target is rejected");
|
|
|
|
const stepTraining = savePublicStepTrainingConfig(state, DRIVER_ONE_TOKEN, { classId: "class_1", steps: [{ effort: "calm", lapCount: 3 }, { effort: "controlled", lapCount: 2 }] });
|
|
assert.equal(stepTraining.driverId, "driver_1");
|
|
assert.deepEqual(stepTraining.steps, [{ effort: "calm", lapCount: 3 }, { effort: "controlled", lapCount: 2 }]);
|
|
assert.equal(savePublicStepTrainingConfig(state, DRIVER_TWO_TOKEN, { classId: "class_2", steps: [{ effort: "calm", lapCount: 3 }] }), null, "driver_2 is not in class_2, unlike class_1 which they share with driver_1");
|
|
assert.equal(savePublicStepTrainingConfig(state, DRIVER_ONE_TOKEN, { classId: "class_1", steps: [] }), null, "a sequence with no laps is rejected");
|
|
});
|
|
|
|
test("public profile computes even-laps and step-training progress from their own since-marker, never leaking raw lap rows", () => {
|
|
const now = Date.now();
|
|
const state = buildState(now);
|
|
state.training.evenLaps = [{ id: "e1", driverId: "driver_1", classId: "class_1", targetMs: 16000, toleranceMs: 500, lapCount: 5 }];
|
|
state.training.stepTraining = [{ id: "s1", driverId: "driver_1", classId: "class_1", steps: [{ effort: "calm", lapCount: 1 }, { effort: "controlled", lapCount: 1 }] }];
|
|
// buildState's class_1 ledger for driver_1, oldest to newest: 19000, 18000, 16500, 16000
|
|
// (the 500ms lap is dropped by the ">500ms" pace filter applied before classLaps is built).
|
|
// A since-marker far enough back captures all four.
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "", 0, 0, now - 2_000_000, now - 2_000_000);
|
|
const classOne = profile.classProfiles.find((item) => item.classId === "class_1");
|
|
|
|
assert.deepEqual(classOne.evenLapsConfig, { targetMs: 16000, toleranceMs: 500, lapCount: 5 });
|
|
// 16000 and 16500 are both within 16000+/-500 of the target; 18000 before them is not.
|
|
assert.equal(classOne.evenLapsProgress.currentStreak, 2);
|
|
assert.equal(classOne.evenLapsProgress.active, true);
|
|
|
|
assert.deepEqual(classOne.stepTrainingConfig, { steps: [{ effort: "calm", lapCount: 1 }, { effort: "controlled", lapCount: 1 }] });
|
|
assert.equal(classOne.stepTrainingProgress.complete, true, "both configured laps (out of four recorded since the marker) are filled");
|
|
assert.equal(classOne.stepTrainingProgress.steps[0].averageLapMs, 19000);
|
|
assert.equal(classOne.stepTrainingProgress.steps[1].averageLapMs, 18000);
|
|
|
|
const serialized = JSON.stringify(profile);
|
|
assert.equal(serialized.includes("driver_1"), false, "the internal driver id must never reach the public payload, including in the new exercise fields");
|
|
});
|
|
|
|
test("public profile omits exercise progress and configuration when the driver has not configured or started one", () => {
|
|
const now = Date.now();
|
|
const profile = buildPublicTrainingProfile(buildState(now), DRIVER_ONE_TOKEN, "", 0, 0);
|
|
const classOne = profile.classProfiles.find((item) => item.classId === "class_1");
|
|
assert.equal(classOne.evenLapsConfig, null);
|
|
assert.equal(classOne.evenLapsProgress, null);
|
|
assert.equal(classOne.stepTrainingConfig, null);
|
|
assert.equal(classOne.stepTrainingProgress, null);
|
|
});
|
|
|
|
test("public profile contains only the selected driver's sanitized aggregates", () => {
|
|
const now = Date.now();
|
|
const profile = buildPublicTrainingProfile(buildState(now), DRIVER_ONE_TOKEN, "2026-08-24T12:00:00.000Z", now - 30000, 30000);
|
|
|
|
assert.equal(profile.profile.name, "Driver One");
|
|
assert.equal(profile.profile.brand, "Associated 2WD");
|
|
assert.equal(profile.profile.className, "2WD");
|
|
assert.equal("clubRank" in profile.profile, false);
|
|
assert.equal("rankedDrivers" in profile.profile, false);
|
|
assert.equal(profile.branding.clubName, "Test Club");
|
|
assert.equal(profile.branding.logoDataUrl, "");
|
|
assert.equal(profile.stats.totalLaps, 4);
|
|
assert.equal(profile.stats.bestLapMs, 16000);
|
|
assert.deepEqual(profile.currentSession, { laps: 2, bestLapMs: 16000, averageLapMs: 16250 });
|
|
assert.equal(profile.goals.length, 1);
|
|
assert.equal(profile.goals[0].title, "Bronze");
|
|
assert.equal(profile.goals[0].percent, 80);
|
|
assert.equal(profile.classProfiles.length, 2);
|
|
assert.equal(profile.classProfiles[0].stats.totalLaps, 4);
|
|
assert.equal(profile.classProfiles[0].stats.achievements.find((item) => item.id === "first_session").unlocked, true);
|
|
assert.ok(Array.isArray(profile.classProfiles[0].stats.paceBuckets));
|
|
assert.deepEqual(profile.classProfiles[0].channelStats, {
|
|
splitCount: 2,
|
|
bestSplitMs: 7000,
|
|
averageSplitMs: 7100,
|
|
latestSplitGainMs: 200,
|
|
pitStopCount: 1,
|
|
totalPitMs: 12500,
|
|
bestPitStopMs: 12500,
|
|
averagePitStopMs: 12500,
|
|
});
|
|
assert.equal(profile.classProfiles[1].className, "4WD");
|
|
assert.equal(profile.classProfiles[1].stats.totalLaps, 2);
|
|
assert.equal(profile.classProfiles[1].stats.bestLapMs, 14000);
|
|
assert.equal(profile.classProfiles[1].channelStats.splitCount, 1);
|
|
assert.equal(profile.classProfiles[1].channelStats.pitStopCount, 1);
|
|
|
|
const serialized = JSON.stringify(profile);
|
|
assert.equal(serialized.includes("Driver Two"), false);
|
|
assert.equal(serialized.includes("driver_1"), false);
|
|
assert.equal(serialized.includes("secret-transponder"), false);
|
|
assert.equal(serialized.includes("secret-decoder"), false);
|
|
});
|
|
|
|
test("public profile visibility removes disabled sections from the API payload", () => {
|
|
const state = buildState();
|
|
state.training.shares[0].visibility = { progress: false, achievements: false, performance: false, goals: false, history: false };
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
const classProfile = profile.classProfiles[0];
|
|
|
|
assert.deepEqual(profile.visibility, { progress: false, achievements: false, performance: false, goals: false, history: false });
|
|
assert.equal(classProfile.goals.length, 0);
|
|
assert.deepEqual(classProfile.channelStats, {});
|
|
assert.equal("paceHistory" in classProfile.stats, false);
|
|
assert.equal("achievements" in classProfile.stats, false);
|
|
assert.equal("stintSummaries" in classProfile.stats, false);
|
|
assert.equal("dailySummaries" in classProfile.stats, false);
|
|
assert.equal(classProfile.stats.totalLaps, 4);
|
|
assert.equal(classProfile.stats.bestLapMs, 16000);
|
|
});
|
|
test("public profile exposes only class-scoped development trends and achievements", () => {
|
|
const now = Date.now();
|
|
const state = buildState(now);
|
|
state.training.ledger = Array.from({ length: 60 }, (_, index) => ({
|
|
driverId: "driver_1",
|
|
sessionId: "practice_2wd",
|
|
timestamp: now - (60 - index) * 18_000,
|
|
lapMs: index < 30 ? 18_000 : 17_000,
|
|
}));
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
const twoWheelDrive = profile.classProfiles.find((item) => item.classId === "class_1");
|
|
const fourWheelDrive = profile.classProfiles.find((item) => item.classId === "class_2");
|
|
|
|
assert.equal(twoWheelDrive.stats.trendSampleReady, true);
|
|
assert.equal(twoWheelDrive.stats.averageTrendMs, 1_000);
|
|
assert.equal(twoWheelDrive.stats.achievements.find((item) => item.id === "controlled_5").unlocked, true);
|
|
assert.equal(fourWheelDrive.stats.totalLaps, 0);
|
|
assert.equal(fourWheelDrive.stats.trendSampleReady, false);
|
|
});
|
|
|
|
test("public timer window freezes aggregates after the personal session", () => {
|
|
const now = Date.now();
|
|
const profile = buildPublicTrainingProfile(buildState(now), DRIVER_ONE_TOKEN, "", now - 30 * 60000, 20 * 60000);
|
|
|
|
assert.deepEqual(profile.currentSession, { laps: 1, bestLapMs: 19000, averageLapMs: 19000 });
|
|
});
|
|
|
|
test("public profile returns null when token or driver is unavailable", () => {
|
|
const state = buildState();
|
|
assert.equal(buildPublicTrainingProfile(state, DISABLED_TOKEN, ""), null);
|
|
state.drivers = state.drivers.filter((driver) => driver.id !== "driver_1");
|
|
assert.equal(buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, ""), null);
|
|
});
|
|
|
|
test("public timer query normalization bounds age and window values", () => {
|
|
const before = Date.now();
|
|
const since = normalizePublicTrainingSince(undefined, "30000");
|
|
const after = Date.now();
|
|
assert.ok(since >= before - 30000 && since <= after - 30000);
|
|
assert.equal(normalizePublicTrainingSince(undefined, String(25 * 60 * 60 * 1000)), 0);
|
|
assert.equal(normalizePublicTrainingWindow("-100"), 0);
|
|
assert.equal(normalizePublicTrainingWindow(String(25 * 60 * 60 * 1000)), 24 * 60 * 60 * 1000);
|
|
});
|
|
|
|
test("public profile counts a max-boundary lap but excludes it from pace", () => {
|
|
const now = Date.now();
|
|
const state = buildState(now);
|
|
state.training.ledger = [
|
|
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: now - 127_000, lapMs: 37_000 },
|
|
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: now - 90_000, lapMs: 37_000 },
|
|
{ driverId: "driver_1", sourceId: "tp_1", sessionId: "practice", timestamp: now, lapMs: 90_000, excludedFromPace: true, stintBoundary: true },
|
|
];
|
|
|
|
const profile = buildPublicTrainingProfile(state, DRIVER_ONE_TOKEN, "");
|
|
assert.equal(profile.stats.totalLaps, 3);
|
|
assert.equal(profile.stats.bestLapMs, 37_000);
|
|
assert.equal(profile.stats.averageLapMs, 37_000);
|
|
assert.equal(profile.stats.stintCount, 2);
|
|
});
|
|
|
|
test("public five-minute result never combines simultaneous transponder sources", () => {
|
|
const laps = ["tp_a", "tp_b"].flatMap((sourceId) => Array.from({ length: 9 }, (_, index) => ({
|
|
sourceId,
|
|
sessionId: "practice_1",
|
|
timestamp: (index + 1) * 37_000,
|
|
lapMs: 37_000,
|
|
})));
|
|
const result = getPublicBestDurationResult(splitPublicTrainingStints(laps), 300_000);
|
|
|
|
assert.equal(result.lapCount, 9);
|
|
assert.equal(result.totalMs, 333_000);
|
|
});
|
|
|
|
test("public profile duration result includes the lap started before five minutes", () => {
|
|
const laps = Array.from({ length: 16 }, (_, index) => ({
|
|
sessionId: "practice_1",
|
|
timestamp: (index + 1) * 18_000,
|
|
lapMs: 18_000,
|
|
}));
|
|
laps.push({ sessionId: "practice_1", timestamp: 314_123, lapMs: 26_123 });
|
|
const result = getPublicBestDurationResult(splitPublicTrainingStints(laps), 300_000);
|
|
|
|
assert.equal(result.lapCount, 17);
|
|
assert.equal(result.totalMs, 314_123);
|
|
assert.equal(result.averageLapMs, 18_478);
|
|
});
|