From 30559e34c290c3f27cc70f4ead99791afd025ab8 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Sat, 6 Jun 2026 16:27:05 -0700 Subject: [PATCH 1/2] Add comprehensive test suites, global error handler, and exercise seed script --- backend/app.js | 4 + backend/coverage/clover.xml | 332 ++++++++ backend/coverage/coverage-final.json | 14 + .../coverage/lcov-report/backend/app.js.html | 229 ++++++ .../coverage/lcov-report/backend/index.html | 116 +++ .../backend/middleware/authMiddleware.js.html | 151 ++++ .../backend/middleware/errorHandler.js.html | 121 +++ .../lcov-report/backend/middleware/index.html | 146 ++++ .../backend/middleware/rateLimiters.js.html | 181 +++++ .../backend/models/Exercise.js.html | 121 +++ .../lcov-report/backend/models/Split.js.html | 160 ++++ .../lcov-report/backend/models/User.js.html | 241 ++++++ .../backend/models/Workout.js.html | 160 ++++ .../lcov-report/backend/models/index.html | 161 ++++ .../lcov-report/backend/routes/ai.js.html | 724 ++++++++++++++++++ .../lcov-report/backend/routes/auth.js.html | 436 +++++++++++ .../backend/routes/exercises.js.html | 181 +++++ .../lcov-report/backend/routes/index.html | 176 +++++ .../lcov-report/backend/routes/splits.js.html | 472 ++++++++++++ .../backend/routes/workouts.js.html | 436 +++++++++++ backend/coverage/lcov-report/base.css | 224 ++++++ .../coverage/lcov-report/block-navigation.js | 87 +++ backend/coverage/lcov-report/favicon.png | Bin 0 -> 445 bytes backend/coverage/lcov-report/index.html | 161 ++++ backend/coverage/lcov-report/prettify.css | 1 + backend/coverage/lcov-report/prettify.js | 2 + .../lcov-report/sort-arrow-sprite.png | Bin 0 -> 138 bytes backend/coverage/lcov-report/sorter.js | 210 +++++ backend/coverage/lcov.info | 597 +++++++++++++++ backend/middleware/authMiddleware.js | 42 +- backend/middleware/errorHandler.js | 13 + backend/middleware/rateLimiters.js | 20 +- backend/models/User.js | 4 - backend/package-lock.json | 10 + backend/package.json | 10 +- backend/routes/ai.js | 245 +++--- backend/routes/auth.js | 16 +- backend/routes/exercises.js | 42 +- backend/routes/splits.js | 54 +- backend/routes/workouts.js | 31 +- backend/scripts/seedExercises.js | 22 + backend/tests/ai.test.js | 113 +++ backend/tests/auth.test.js | 54 ++ backend/tests/errorHandler.test.js | 28 + backend/tests/exercises.test.js | 28 + backend/tests/security.test.js | 29 +- backend/tests/splits.test.js | 124 +++ backend/tests/workouts.test.js | 122 +++ frontend/src/apis/aiApi.test.js | 41 + frontend/src/apis/authApi.test.js | 51 ++ frontend/src/apis/authHeaders.test.js | 15 + frontend/src/apis/exerciseApi.test.js | 34 + frontend/src/apis/splitApi.js | 11 + frontend/src/apis/splitApi.test.js | 75 ++ frontend/src/apis/workoutApi.test.js | 73 ++ .../components/SplitLibrary/SplitLibrary.css | 68 +- .../components/SplitLibrary/SplitLibrary.js | 97 ++- .../SplitLibrary/SplitLibrary.test.js | 33 + frontend/src/context/AuthContext.test.js | 22 + 59 files changed, 7046 insertions(+), 325 deletions(-) create mode 100644 backend/coverage/clover.xml create mode 100644 backend/coverage/coverage-final.json create mode 100644 backend/coverage/lcov-report/backend/app.js.html create mode 100644 backend/coverage/lcov-report/backend/index.html create mode 100644 backend/coverage/lcov-report/backend/middleware/authMiddleware.js.html create mode 100644 backend/coverage/lcov-report/backend/middleware/errorHandler.js.html create mode 100644 backend/coverage/lcov-report/backend/middleware/index.html create mode 100644 backend/coverage/lcov-report/backend/middleware/rateLimiters.js.html create mode 100644 backend/coverage/lcov-report/backend/models/Exercise.js.html create mode 100644 backend/coverage/lcov-report/backend/models/Split.js.html create mode 100644 backend/coverage/lcov-report/backend/models/User.js.html create mode 100644 backend/coverage/lcov-report/backend/models/Workout.js.html create mode 100644 backend/coverage/lcov-report/backend/models/index.html create mode 100644 backend/coverage/lcov-report/backend/routes/ai.js.html create mode 100644 backend/coverage/lcov-report/backend/routes/auth.js.html create mode 100644 backend/coverage/lcov-report/backend/routes/exercises.js.html create mode 100644 backend/coverage/lcov-report/backend/routes/index.html create mode 100644 backend/coverage/lcov-report/backend/routes/splits.js.html create mode 100644 backend/coverage/lcov-report/backend/routes/workouts.js.html create mode 100644 backend/coverage/lcov-report/base.css create mode 100644 backend/coverage/lcov-report/block-navigation.js create mode 100644 backend/coverage/lcov-report/favicon.png create mode 100644 backend/coverage/lcov-report/index.html create mode 100644 backend/coverage/lcov-report/prettify.css create mode 100644 backend/coverage/lcov-report/prettify.js create mode 100644 backend/coverage/lcov-report/sort-arrow-sprite.png create mode 100644 backend/coverage/lcov-report/sorter.js create mode 100644 backend/coverage/lcov.info create mode 100644 backend/middleware/errorHandler.js create mode 100644 backend/scripts/seedExercises.js create mode 100644 backend/tests/ai.test.js create mode 100644 backend/tests/errorHandler.test.js create mode 100644 backend/tests/exercises.test.js create mode 100644 backend/tests/splits.test.js create mode 100644 backend/tests/workouts.test.js create mode 100644 frontend/src/apis/aiApi.test.js create mode 100644 frontend/src/apis/authApi.test.js create mode 100644 frontend/src/apis/authHeaders.test.js create mode 100644 frontend/src/apis/exerciseApi.test.js create mode 100644 frontend/src/apis/splitApi.test.js create mode 100644 frontend/src/apis/workoutApi.test.js create mode 100644 frontend/src/components/SplitLibrary/SplitLibrary.test.js create mode 100644 frontend/src/context/AuthContext.test.js diff --git a/backend/app.js b/backend/app.js index c451fa6..c1b51c7 100644 --- a/backend/app.js +++ b/backend/app.js @@ -3,6 +3,7 @@ const cors = require("cors"); const helmet = require("helmet"); const bodyParser = require("body-parser"); const path = require("path"); +require("express-async-errors"); const exerciseRoutes = require("./routes/exercises"); const workoutRoutes = require("./routes/workouts"); @@ -42,4 +43,7 @@ app.get("*", (req, res) => { res.sendFile(path.join(__dirname, "../frontend/build", "index.html")); }); +const errorHandler = require("./middleware/errorHandler"); +app.use(errorHandler); + module.exports = app; \ No newline at end of file diff --git a/backend/coverage/clover.xml b/backend/coverage/clover.xml new file mode 100644 index 0000000..3276ca5 --- /dev/null +++ b/backend/coverage/clover.xml @@ -0,0 +1,332 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/coverage/coverage-final.json b/backend/coverage/coverage-final.json new file mode 100644 index 0000000..eb10fe9 --- /dev/null +++ b/backend/coverage/coverage-final.json @@ -0,0 +1,14 @@ +{"/mnt/c/users/Nathan Huang/FitForge/backend/app.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/app.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":13},"end":{"line":2,"column":28}},"2":{"start":{"line":3,"column":15},"end":{"line":3,"column":32}},"3":{"start":{"line":4,"column":19},"end":{"line":4,"column":41}},"4":{"start":{"line":5,"column":13},"end":{"line":5,"column":28}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":32}},"6":{"start":{"line":8,"column":23},"end":{"line":8,"column":52}},"7":{"start":{"line":9,"column":22},"end":{"line":9,"column":50}},"8":{"start":{"line":10,"column":20},"end":{"line":10,"column":46}},"9":{"start":{"line":11,"column":19},"end":{"line":11,"column":43}},"10":{"start":{"line":12,"column":17},"end":{"line":12,"column":39}},"11":{"start":{"line":13,"column":47},"end":{"line":13,"column":83}},"12":{"start":{"line":15,"column":12},"end":{"line":15,"column":21}},"13":{"start":{"line":17,"column":23},"end":{"line":20,"column":18}},"14":{"start":{"line":19,"column":19},"end":{"line":19,"column":32}},"15":{"start":{"line":22,"column":0},"end":{"line":22,"column":50}},"16":{"start":{"line":23,"column":0},"end":{"line":30,"column":2}},"17":{"start":{"line":26,"column":6},"end":{"line":26,"column":82}},"18":{"start":{"line":26,"column":54},"end":{"line":26,"column":82}},"19":{"start":{"line":27,"column":6},"end":{"line":27,"column":56}},"20":{"start":{"line":31,"column":0},"end":{"line":31,"column":43}},"21":{"start":{"line":33,"column":0},"end":{"line":33,"column":28}},"22":{"start":{"line":34,"column":0},"end":{"line":34,"column":42}},"23":{"start":{"line":35,"column":0},"end":{"line":35,"column":40}},"24":{"start":{"line":36,"column":0},"end":{"line":36,"column":36}},"25":{"start":{"line":37,"column":0},"end":{"line":37,"column":46}},"26":{"start":{"line":38,"column":0},"end":{"line":38,"column":40}},"27":{"start":{"line":41,"column":0},"end":{"line":41,"column":67}},"28":{"start":{"line":42,"column":0},"end":{"line":44,"column":3}},"29":{"start":{"line":43,"column":2},"end":{"line":43,"column":72}},"30":{"start":{"line":46,"column":21},"end":{"line":46,"column":57}},"31":{"start":{"line":47,"column":0},"end":{"line":47,"column":22}},"32":{"start":{"line":49,"column":0},"end":{"line":49,"column":21}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":19,"column":7},"end":{"line":19,"column":8}},"loc":{"start":{"line":19,"column":19},"end":{"line":19,"column":32}},"line":19},"1":{"name":"(anonymous_1)","decl":{"start":{"line":25,"column":12},"end":{"line":25,"column":13}},"loc":{"start":{"line":25,"column":34},"end":{"line":28,"column":5}},"line":25},"2":{"name":"(anonymous_2)","decl":{"start":{"line":42,"column":13},"end":{"line":42,"column":14}},"loc":{"start":{"line":42,"column":27},"end":{"line":44,"column":1}},"line":42}},"branchMap":{"0":{"loc":{"start":{"line":17,"column":24},"end":{"line":17,"column":74}},"type":"binary-expr","locations":[{"start":{"line":17,"column":24},"end":{"line":17,"column":47}},{"start":{"line":17,"column":51},"end":{"line":17,"column":74}}],"line":17},"1":{"loc":{"start":{"line":26,"column":6},"end":{"line":26,"column":82}},"type":"if","locations":[{"start":{"line":26,"column":6},"end":{"line":26,"column":82}},{"start":{},"end":{}}],"line":26},"2":{"loc":{"start":{"line":26,"column":10},"end":{"line":26,"column":52}},"type":"binary-expr","locations":[{"start":{"line":26,"column":10},"end":{"line":26,"column":17}},{"start":{"line":26,"column":21},"end":{"line":26,"column":52}}],"line":26}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":6,"8":6,"9":6,"10":6,"11":6,"12":6,"13":6,"14":6,"15":6,"16":6,"17":143,"18":142,"19":1,"20":6,"21":6,"22":6,"23":6,"24":6,"25":6,"26":6,"27":6,"28":6,"29":1,"30":6,"31":6,"32":6},"f":{"0":6,"1":143,"2":1},"b":{"0":[6,6],"1":[142,1],"2":[143,1]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"1e5990567cf2a5149fed8bfe4eb251114ba02ae8"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/authMiddleware.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/authMiddleware.js","statementMap":{"0":{"start":{"line":1,"column":12},"end":{"line":1,"column":35}},"1":{"start":{"line":2,"column":13},"end":{"line":2,"column":38}},"2":{"start":{"line":4,"column":16},"end":{"line":21,"column":1}},"3":{"start":{"line":5,"column":2},"end":{"line":20,"column":3}},"4":{"start":{"line":6,"column":23},"end":{"line":6,"column":48}},"5":{"start":{"line":7,"column":4},"end":{"line":9,"column":5}},"6":{"start":{"line":8,"column":6},"end":{"line":8,"column":75}},"7":{"start":{"line":10,"column":18},"end":{"line":10,"column":42}},"8":{"start":{"line":11,"column":20},"end":{"line":11,"column":61}},"9":{"start":{"line":12,"column":17},"end":{"line":12,"column":76}},"10":{"start":{"line":13,"column":4},"end":{"line":15,"column":5}},"11":{"start":{"line":14,"column":6},"end":{"line":14,"column":81}},"12":{"start":{"line":16,"column":4},"end":{"line":16,"column":20}},"13":{"start":{"line":17,"column":4},"end":{"line":17,"column":11}},"14":{"start":{"line":19,"column":4},"end":{"line":19,"column":77}},"15":{"start":{"line":23,"column":0},"end":{"line":23,"column":29}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":4,"column":16},"end":{"line":4,"column":17}},"loc":{"start":{"line":4,"column":42},"end":{"line":21,"column":1}},"line":4}},"branchMap":{"0":{"loc":{"start":{"line":7,"column":4},"end":{"line":9,"column":5}},"type":"if","locations":[{"start":{"line":7,"column":4},"end":{"line":9,"column":5}},{"start":{},"end":{}}],"line":7},"1":{"loc":{"start":{"line":7,"column":8},"end":{"line":7,"column":56}},"type":"binary-expr","locations":[{"start":{"line":7,"column":8},"end":{"line":7,"column":19}},{"start":{"line":7,"column":23},"end":{"line":7,"column":56}}],"line":7},"2":{"loc":{"start":{"line":13,"column":4},"end":{"line":15,"column":5}},"type":"if","locations":[{"start":{"line":13,"column":4},"end":{"line":15,"column":5}},{"start":{},"end":{}}],"line":13}},"s":{"0":6,"1":6,"2":6,"3":76,"4":76,"5":76,"6":8,"7":68,"8":68,"9":66,"10":66,"11":1,"12":65,"13":65,"14":2,"15":6},"f":{"0":76},"b":{"0":[8,68],"1":[76,69],"2":[1,65]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"ae932a65be686fc48b0466be89a122e6462672f7"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/errorHandler.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/errorHandler.js","statementMap":{"0":{"start":{"line":2,"column":21},"end":{"line":11,"column":1}},"1":{"start":{"line":3,"column":2},"end":{"line":5,"column":3}},"2":{"start":{"line":4,"column":4},"end":{"line":4,"column":66}},"3":{"start":{"line":6,"column":2},"end":{"line":8,"column":3}},"4":{"start":{"line":7,"column":4},"end":{"line":7,"column":58}},"5":{"start":{"line":9,"column":2},"end":{"line":9,"column":49}},"6":{"start":{"line":10,"column":2},"end":{"line":10,"column":52}},"7":{"start":{"line":13,"column":0},"end":{"line":13,"column":30}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":2,"column":21},"end":{"line":2,"column":22}},"loc":{"start":{"line":2,"column":46},"end":{"line":11,"column":1}},"line":2}},"branchMap":{"0":{"loc":{"start":{"line":3,"column":2},"end":{"line":5,"column":3}},"type":"if","locations":[{"start":{"line":3,"column":2},"end":{"line":5,"column":3}},{"start":{},"end":{}}],"line":3},"1":{"loc":{"start":{"line":6,"column":2},"end":{"line":8,"column":3}},"type":"if","locations":[{"start":{"line":6,"column":2},"end":{"line":8,"column":3}},{"start":{},"end":{}}],"line":6}},"s":{"0":7,"1":7,"2":2,"3":5,"4":1,"5":4,"6":4,"7":7},"f":{"0":7},"b":{"0":[2,5],"1":[1,4]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"bafc6316c9df86ff5f15cbcde7361ecae6a230c0"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/rateLimiters.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/rateLimiters.js","statementMap":{"0":{"start":{"line":1,"column":18},"end":{"line":1,"column":47}},"1":{"start":{"line":4,"column":13},"end":{"line":4,"column":50}},"2":{"start":{"line":4,"column":19},"end":{"line":4,"column":50}},"3":{"start":{"line":6,"column":19},"end":{"line":13,"column":2}},"4":{"start":{"line":15,"column":20},"end":{"line":22,"column":2}},"5":{"start":{"line":24,"column":18},"end":{"line":31,"column":2}},"6":{"start":{"line":33,"column":0},"end":{"line":33,"column":56}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":4,"column":13},"end":{"line":4,"column":14}},"loc":{"start":{"line":4,"column":19},"end":{"line":4,"column":50}},"line":4}},"branchMap":{},"s":{"0":6,"1":6,"2":223,"3":6,"4":6,"5":6,"6":6},"f":{"0":223},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"3f67edeba234e7eb1b887984ea4aca0ca4bd4ea2"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/models/Exercise.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/models/Exercise.js","statementMap":{"0":{"start":{"line":1,"column":17},"end":{"line":1,"column":36}},"1":{"start":{"line":3,"column":23},"end":{"line":10,"column":2}},"2":{"start":{"line":12,"column":0},"end":{"line":12,"column":60}}},"fnMap":{},"branchMap":{},"s":{"0":6,"1":6,"2":6},"f":{},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"9c84d48d925448b94d12a2108ed1b1805ce5de9f"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/models/Split.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/models/Split.js","statementMap":{"0":{"start":{"line":1,"column":17},"end":{"line":1,"column":36}},"1":{"start":{"line":3,"column":20},"end":{"line":23,"column":1}},"2":{"start":{"line":25,"column":0},"end":{"line":25,"column":54}}},"fnMap":{},"branchMap":{},"s":{"0":6,"1":6,"2":6},"f":{},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"9fa32a48c7013c75253e9b2b24b8e61cc675cf5b"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/models/User.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/models/User.js","statementMap":{"0":{"start":{"line":1,"column":17},"end":{"line":1,"column":36}},"1":{"start":{"line":3,"column":19},"end":{"line":51,"column":1}},"2":{"start":{"line":53,"column":0},"end":{"line":53,"column":52}}},"fnMap":{},"branchMap":{},"s":{"0":6,"1":6,"2":6},"f":{},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"53b3c5f18cdefe3ddd1efca799813f0365a1d04f"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/models/Workout.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/models/Workout.js","statementMap":{"0":{"start":{"line":1,"column":17},"end":{"line":1,"column":36}},"1":{"start":{"line":3,"column":22},"end":{"line":23,"column":1}},"2":{"start":{"line":25,"column":0},"end":{"line":25,"column":58}}},"fnMap":{},"branchMap":{},"s":{"0":6,"1":6,"2":6},"f":{},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"c7cb1b034e1eabf1c6530a855f4ebad5364f5eff"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/ai.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/ai.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":17},"end":{"line":2,"column":46}},"2":{"start":{"line":3,"column":20},"end":{"line":3,"column":59}},"3":{"start":{"line":4,"column":16},"end":{"line":4,"column":44}},"4":{"start":{"line":5,"column":14},"end":{"line":5,"column":40}},"5":{"start":{"line":7,"column":15},"end":{"line":7,"column":31}},"6":{"start":{"line":9,"column":20},"end":{"line":18,"column":1}},"7":{"start":{"line":10,"column":21},"end":{"line":10,"column":38}},"8":{"start":{"line":11,"column":20},"end":{"line":11,"column":41}},"9":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"10":{"start":{"line":14,"column":4},"end":{"line":14,"column":57}},"11":{"start":{"line":17,"column":2},"end":{"line":17,"column":59}},"12":{"start":{"line":20,"column":0},"end":{"line":178,"column":3}},"13":{"start":{"line":21,"column":18},"end":{"line":21,"column":47}},"14":{"start":{"line":30,"column":6},"end":{"line":30,"column":13}},"15":{"start":{"line":33,"column":4},"end":{"line":39,"column":10}},"16":{"start":{"line":36,"column":41},"end":{"line":36,"column":62}},"17":{"start":{"line":41,"column":20},"end":{"line":43,"column":48}},"18":{"start":{"line":45,"column":2},"end":{"line":49,"column":3}},"19":{"start":{"line":46,"column":4},"end":{"line":48,"column":7}},"20":{"start":{"line":51,"column":26},"end":{"line":57,"column":5}},"21":{"start":{"line":51,"column":55},"end":{"line":57,"column":3}},"22":{"start":{"line":59,"column":23},"end":{"line":72,"column":1}},"23":{"start":{"line":74,"column":21},"end":{"line":112,"column":1}},"24":{"start":{"line":114,"column":22},"end":{"line":131,"column":4}},"25":{"start":{"line":133,"column":18},"end":{"line":133,"column":42}},"26":{"start":{"line":135,"column":2},"end":{"line":150,"column":3}},"27":{"start":{"line":136,"column":4},"end":{"line":136,"column":53}},"28":{"start":{"line":137,"column":4},"end":{"line":137,"column":74}},"29":{"start":{"line":138,"column":4},"end":{"line":138,"column":111}},"30":{"start":{"line":139,"column":4},"end":{"line":139,"column":107}},"31":{"start":{"line":140,"column":4},"end":{"line":140,"column":75}},"32":{"start":{"line":142,"column":4},"end":{"line":149,"column":7}},"33":{"start":{"line":152,"column":18},"end":{"line":152,"column":56}},"34":{"start":{"line":154,"column":2},"end":{"line":158,"column":3}},"35":{"start":{"line":155,"column":4},"end":{"line":157,"column":7}},"36":{"start":{"line":160,"column":15},"end":{"line":160,"column":35}},"37":{"start":{"line":162,"column":27},"end":{"line":162,"column":82}},"38":{"start":{"line":162,"column":69},"end":{"line":162,"column":80}},"39":{"start":{"line":164,"column":2},"end":{"line":173,"column":3}},"40":{"start":{"line":165,"column":4},"end":{"line":172,"column":5}},"41":{"start":{"line":166,"column":6},"end":{"line":171,"column":7}},"42":{"start":{"line":167,"column":8},"end":{"line":170,"column":11}},"43":{"start":{"line":175,"column":2},"end":{"line":177,"column":5}},"44":{"start":{"line":180,"column":0},"end":{"line":212,"column":3}},"45":{"start":{"line":181,"column":19},"end":{"line":181,"column":27}},"46":{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},"47":{"start":{"line":184,"column":4},"end":{"line":184,"column":71}},"48":{"start":{"line":187,"column":26},"end":{"line":187,"column":28}},"49":{"start":{"line":189,"column":2},"end":{"line":199,"column":3}},"50":{"start":{"line":190,"column":24},"end":{"line":190,"column":84}},"51":{"start":{"line":190,"column":64},"end":{"line":190,"column":83}},"52":{"start":{"line":192,"column":20},"end":{"line":196,"column":6}},"53":{"start":{"line":198,"column":4},"end":{"line":198,"column":34}},"54":{"start":{"line":201,"column":16},"end":{"line":205,"column":4}},"55":{"start":{"line":204,"column":47},"end":{"line":204,"column":58}},"56":{"start":{"line":207,"column":2},"end":{"line":211,"column":5}},"57":{"start":{"line":214,"column":0},"end":{"line":214,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":20},"end":{"line":9,"column":21}},"loc":{"start":{"line":9,"column":30},"end":{"line":18,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":20,"column":42},"end":{"line":20,"column":43}},"loc":{"start":{"line":20,"column":62},"end":{"line":178,"column":1}},"line":20},"2":{"name":"(anonymous_2)","decl":{"start":{"line":36,"column":31},"end":{"line":36,"column":32}},"loc":{"start":{"line":36,"column":41},"end":{"line":36,"column":62}},"line":36},"3":{"name":"(anonymous_3)","decl":{"start":{"line":51,"column":40},"end":{"line":51,"column":41}},"loc":{"start":{"line":51,"column":55},"end":{"line":57,"column":3}},"line":51},"4":{"name":"(anonymous_4)","decl":{"start":{"line":162,"column":55},"end":{"line":162,"column":56}},"loc":{"start":{"line":162,"column":69},"end":{"line":162,"column":80}},"line":162},"5":{"name":"(anonymous_5)","decl":{"start":{"line":180,"column":45},"end":{"line":180,"column":46}},"loc":{"start":{"line":180,"column":65},"end":{"line":212,"column":1}},"line":180},"6":{"name":"(anonymous_6)","decl":{"start":{"line":190,"column":50},"end":{"line":190,"column":51}},"loc":{"start":{"line":190,"column":64},"end":{"line":190,"column":83}},"line":190},"7":{"name":"(anonymous_7)","decl":{"start":{"line":204,"column":34},"end":{"line":204,"column":35}},"loc":{"start":{"line":204,"column":47},"end":{"line":204,"column":58}},"line":204}},"branchMap":{"0":{"loc":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"type":"if","locations":[{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},{"start":{},"end":{}}],"line":13},"1":{"loc":{"start":{"line":13,"column":6},"end":{"line":13,"column":43}},"type":"binary-expr","locations":[{"start":{"line":13,"column":6},"end":{"line":13,"column":23}},{"start":{"line":13,"column":27},"end":{"line":13,"column":43}}],"line":13},"2":{"loc":{"start":{"line":21,"column":18},"end":{"line":21,"column":47}},"type":"binary-expr","locations":[{"start":{"line":21,"column":18},"end":{"line":21,"column":41}},{"start":{"line":21,"column":45},"end":{"line":21,"column":47}}],"line":21},"3":{"loc":{"start":{"line":24,"column":4},"end":{"line":24,"column":28}},"type":"default-arg","locations":[{"start":{"line":24,"column":11},"end":{"line":24,"column":28}}],"line":24},"4":{"loc":{"start":{"line":25,"column":4},"end":{"line":25,"column":32}},"type":"default-arg","locations":[{"start":{"line":25,"column":22},"end":{"line":25,"column":32}}],"line":25},"5":{"loc":{"start":{"line":26,"column":4},"end":{"line":26,"column":19}},"type":"default-arg","locations":[{"start":{"line":26,"column":18},"end":{"line":26,"column":19}}],"line":26},"6":{"loc":{"start":{"line":27,"column":4},"end":{"line":27,"column":29}},"type":"default-arg","locations":[{"start":{"line":27,"column":27},"end":{"line":27,"column":29}}],"line":27},"7":{"loc":{"start":{"line":28,"column":4},"end":{"line":28,"column":18}},"type":"default-arg","locations":[{"start":{"line":28,"column":16},"end":{"line":28,"column":18}}],"line":28},"8":{"loc":{"start":{"line":29,"column":4},"end":{"line":29,"column":20}},"type":"default-arg","locations":[{"start":{"line":29,"column":18},"end":{"line":29,"column":20}}],"line":29},"9":{"loc":{"start":{"line":33,"column":4},"end":{"line":39,"column":10}},"type":"cond-expr","locations":[{"start":{"line":34,"column":8},"end":{"line":38,"column":9}},{"start":{"line":39,"column":8},"end":{"line":39,"column":10}}],"line":33},"10":{"loc":{"start":{"line":45,"column":2},"end":{"line":49,"column":3}},"type":"if","locations":[{"start":{"line":45,"column":2},"end":{"line":49,"column":3}},{"start":{},"end":{}}],"line":45},"11":{"loc":{"start":{"line":82,"column":15},"end":{"line":82,"column":54}},"type":"binary-expr","locations":[{"start":{"line":82,"column":15},"end":{"line":82,"column":35}},{"start":{"line":82,"column":39},"end":{"line":82,"column":54}}],"line":82},"12":{"loc":{"start":{"line":83,"column":17},"end":{"line":83,"column":49}},"type":"binary-expr","locations":[{"start":{"line":83,"column":17},"end":{"line":83,"column":39}},{"start":{"line":83,"column":43},"end":{"line":83,"column":49}}],"line":83},"13":{"loc":{"start":{"line":135,"column":2},"end":{"line":150,"column":3}},"type":"if","locations":[{"start":{"line":135,"column":2},"end":{"line":150,"column":3}},{"start":{},"end":{}}],"line":135},"14":{"loc":{"start":{"line":142,"column":22},"end":{"line":142,"column":60}},"type":"cond-expr","locations":[{"start":{"line":142,"column":51},"end":{"line":142,"column":54}},{"start":{"line":142,"column":57},"end":{"line":142,"column":60}}],"line":142},"15":{"loc":{"start":{"line":144,"column":8},"end":{"line":146,"column":32}},"type":"cond-expr","locations":[{"start":{"line":145,"column":12},"end":{"line":145,"column":56}},{"start":{"line":146,"column":12},"end":{"line":146,"column":32}}],"line":144},"16":{"loc":{"start":{"line":154,"column":2},"end":{"line":158,"column":3}},"type":"if","locations":[{"start":{"line":154,"column":2},"end":{"line":158,"column":3}},{"start":{},"end":{}}],"line":154},"17":{"loc":{"start":{"line":164,"column":20},"end":{"line":164,"column":35}},"type":"binary-expr","locations":[{"start":{"line":164,"column":20},"end":{"line":164,"column":29}},{"start":{"line":164,"column":33},"end":{"line":164,"column":35}}],"line":164},"18":{"loc":{"start":{"line":165,"column":27},"end":{"line":165,"column":46}},"type":"binary-expr","locations":[{"start":{"line":165,"column":27},"end":{"line":165,"column":40}},{"start":{"line":165,"column":44},"end":{"line":165,"column":46}}],"line":165},"19":{"loc":{"start":{"line":166,"column":6},"end":{"line":171,"column":7}},"type":"if","locations":[{"start":{"line":166,"column":6},"end":{"line":171,"column":7}},{"start":{},"end":{}}],"line":166},"20":{"loc":{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},"type":"if","locations":[{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},{"start":{},"end":{}}],"line":183},"21":{"loc":{"start":{"line":183,"column":6},"end":{"line":183,"column":40}},"type":"binary-expr","locations":[{"start":{"line":183,"column":6},"end":{"line":183,"column":11}},{"start":{"line":183,"column":15},"end":{"line":183,"column":40}}],"line":183},"22":{"loc":{"start":{"line":190,"column":25},"end":{"line":190,"column":44}},"type":"binary-expr","locations":[{"start":{"line":190,"column":25},"end":{"line":190,"column":38}},{"start":{"line":190,"column":42},"end":{"line":190,"column":44}}],"line":190},"23":{"loc":{"start":{"line":194,"column":12},"end":{"line":194,"column":40}},"type":"binary-expr","locations":[{"start":{"line":194,"column":12},"end":{"line":194,"column":20}},{"start":{"line":194,"column":24},"end":{"line":194,"column":40}}],"line":194},"24":{"loc":{"start":{"line":203,"column":10},"end":{"line":203,"column":49}},"type":"binary-expr","locations":[{"start":{"line":203,"column":10},"end":{"line":203,"column":26}},{"start":{"line":203,"column":30},"end":{"line":203,"column":49}}],"line":203}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":4,"8":4,"9":4,"10":1,"11":3,"12":6,"13":8,"14":8,"15":8,"16":1,"17":8,"18":8,"19":1,"20":7,"21":7,"22":7,"23":7,"24":7,"25":6,"26":6,"27":1,"28":1,"29":1,"30":1,"31":1,"32":1,"33":5,"34":5,"35":1,"36":4,"37":3,"38":3,"39":3,"40":3,"41":3,"42":1,"43":2,"44":6,"45":2,"46":2,"47":1,"48":1,"49":1,"50":1,"51":1,"52":1,"53":1,"54":1,"55":1,"56":1,"57":6},"f":{"0":4,"1":8,"2":1,"3":7,"4":3,"5":2,"6":1,"7":1},"b":{"0":[1,3],"1":[4,3],"2":[8,0],"3":[0],"4":[0],"5":[0],"6":[0],"7":[0],"8":[0],"9":[1,7],"10":[1,7],"11":[7,6],"12":[7,7],"13":[1,5],"14":[1,0],"15":[1,0],"16":[1,4],"17":[3,0],"18":[3,0],"19":[1,2],"20":[1,1],"21":[2,2],"22":[1,0],"23":[1,0],"24":[1,0]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"960e258580e978209e861d0f84e7a6cbe9374cd2"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/auth.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/auth.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":15},"end":{"line":2,"column":34}},"2":{"start":{"line":3,"column":12},"end":{"line":3,"column":35}},"3":{"start":{"line":4,"column":13},"end":{"line":4,"column":38}},"4":{"start":{"line":5,"column":20},"end":{"line":5,"column":59}},"5":{"start":{"line":7,"column":15},"end":{"line":7,"column":31}},"6":{"start":{"line":9,"column":20},"end":{"line":13,"column":1}},"7":{"start":{"line":10,"column":4},"end":{"line":12,"column":7}},"8":{"start":{"line":15,"column":22},"end":{"line":20,"column":2}},"9":{"start":{"line":15,"column":33},"end":{"line":20,"column":1}},"10":{"start":{"line":22,"column":0},"end":{"line":52,"column":3}},"11":{"start":{"line":23,"column":42},"end":{"line":23,"column":50}},"12":{"start":{"line":25,"column":8},"end":{"line":27,"column":9}},"13":{"start":{"line":26,"column":12},"end":{"line":26,"column":95}},"14":{"start":{"line":29,"column":8},"end":{"line":31,"column":9}},"15":{"start":{"line":30,"column":12},"end":{"line":30,"column":95}},"16":{"start":{"line":33,"column":29},"end":{"line":33,"column":58}},"17":{"start":{"line":35,"column":8},"end":{"line":37,"column":9}},"18":{"start":{"line":36,"column":12},"end":{"line":36,"column":81}},"19":{"start":{"line":39,"column":29},"end":{"line":39,"column":60}},"20":{"start":{"line":41,"column":21},"end":{"line":45,"column":10}},"21":{"start":{"line":47,"column":8},"end":{"line":50,"column":11}},"22":{"start":{"line":54,"column":0},"end":{"line":77,"column":3}},"23":{"start":{"line":55,"column":36},"end":{"line":55,"column":44}},"24":{"start":{"line":57,"column":8},"end":{"line":59,"column":9}},"25":{"start":{"line":58,"column":12},"end":{"line":58,"column":88}},"26":{"start":{"line":61,"column":21},"end":{"line":61,"column":50}},"27":{"start":{"line":63,"column":8},"end":{"line":65,"column":9}},"28":{"start":{"line":64,"column":12},"end":{"line":64,"column":82}},"29":{"start":{"line":67,"column":32},"end":{"line":67,"column":81}},"30":{"start":{"line":69,"column":8},"end":{"line":71,"column":9}},"31":{"start":{"line":70,"column":12},"end":{"line":70,"column":82}},"32":{"start":{"line":73,"column":8},"end":{"line":76,"column":11}},"33":{"start":{"line":79,"column":0},"end":{"line":83,"column":3}},"34":{"start":{"line":80,"column":4},"end":{"line":82,"column":7}},"35":{"start":{"line":85,"column":0},"end":{"line":116,"column":3}},"36":{"start":{"line":94,"column":12},"end":{"line":94,"column":20}},"37":{"start":{"line":96,"column":21},"end":{"line":96,"column":54}},"38":{"start":{"line":98,"column":8},"end":{"line":100,"column":9}},"39":{"start":{"line":99,"column":12},"end":{"line":99,"column":71}},"40":{"start":{"line":102,"column":8},"end":{"line":110,"column":10}},"41":{"start":{"line":112,"column":8},"end":{"line":112,"column":26}},"42":{"start":{"line":114,"column":8},"end":{"line":114,"column":60}},"43":{"start":{"line":118,"column":0},"end":{"line":118,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":20},"end":{"line":9,"column":21}},"loc":{"start":{"line":9,"column":32},"end":{"line":13,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":15,"column":22},"end":{"line":15,"column":23}},"loc":{"start":{"line":15,"column":33},"end":{"line":20,"column":1}},"line":15},"2":{"name":"(anonymous_2)","decl":{"start":{"line":22,"column":25},"end":{"line":22,"column":26}},"loc":{"start":{"line":22,"column":45},"end":{"line":52,"column":1}},"line":22},"3":{"name":"(anonymous_3)","decl":{"start":{"line":54,"column":22},"end":{"line":54,"column":23}},"loc":{"start":{"line":54,"column":42},"end":{"line":77,"column":1}},"line":54},"4":{"name":"(anonymous_4)","decl":{"start":{"line":79,"column":27},"end":{"line":79,"column":28}},"loc":{"start":{"line":79,"column":47},"end":{"line":83,"column":1}},"line":79},"5":{"name":"(anonymous_5)","decl":{"start":{"line":85,"column":32},"end":{"line":85,"column":33}},"loc":{"start":{"line":85,"column":52},"end":{"line":116,"column":1}},"line":85}},"branchMap":{"0":{"loc":{"start":{"line":25,"column":8},"end":{"line":27,"column":9}},"type":"if","locations":[{"start":{"line":25,"column":8},"end":{"line":27,"column":9}},{"start":{},"end":{}}],"line":25},"1":{"loc":{"start":{"line":25,"column":12},"end":{"line":25,"column":40}},"type":"binary-expr","locations":[{"start":{"line":25,"column":12},"end":{"line":25,"column":17}},{"start":{"line":25,"column":21},"end":{"line":25,"column":27}},{"start":{"line":25,"column":31},"end":{"line":25,"column":40}}],"line":25},"2":{"loc":{"start":{"line":29,"column":8},"end":{"line":31,"column":9}},"type":"if","locations":[{"start":{"line":29,"column":8},"end":{"line":31,"column":9}},{"start":{},"end":{}}],"line":29},"3":{"loc":{"start":{"line":35,"column":8},"end":{"line":37,"column":9}},"type":"if","locations":[{"start":{"line":35,"column":8},"end":{"line":37,"column":9}},{"start":{},"end":{}}],"line":35},"4":{"loc":{"start":{"line":57,"column":8},"end":{"line":59,"column":9}},"type":"if","locations":[{"start":{"line":57,"column":8},"end":{"line":59,"column":9}},{"start":{},"end":{}}],"line":57},"5":{"loc":{"start":{"line":57,"column":12},"end":{"line":57,"column":31}},"type":"binary-expr","locations":[{"start":{"line":57,"column":12},"end":{"line":57,"column":18}},{"start":{"line":57,"column":22},"end":{"line":57,"column":31}}],"line":57},"6":{"loc":{"start":{"line":63,"column":8},"end":{"line":65,"column":9}},"type":"if","locations":[{"start":{"line":63,"column":8},"end":{"line":65,"column":9}},{"start":{},"end":{}}],"line":63},"7":{"loc":{"start":{"line":69,"column":8},"end":{"line":71,"column":9}},"type":"if","locations":[{"start":{"line":69,"column":8},"end":{"line":71,"column":9}},{"start":{},"end":{}}],"line":69},"8":{"loc":{"start":{"line":98,"column":8},"end":{"line":100,"column":9}},"type":"if","locations":[{"start":{"line":98,"column":8},"end":{"line":100,"column":9}},{"start":{},"end":{}}],"line":98}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":56,"8":6,"9":59,"10":6,"11":58,"12":58,"13":1,"14":57,"15":1,"16":56,"17":56,"18":1,"19":55,"20":55,"21":55,"22":6,"23":4,"24":4,"25":1,"26":3,"27":3,"28":1,"29":2,"30":2,"31":1,"32":1,"33":6,"34":1,"35":6,"36":2,"37":2,"38":2,"39":0,"40":2,"41":2,"42":2,"43":6},"f":{"0":56,"1":59,"2":58,"3":4,"4":1,"5":2},"b":{"0":[1,57],"1":[58,57,57],"2":[1,56],"3":[1,55],"4":[1,3],"5":[4,4],"6":[1,2],"7":[1,1],"8":[0,2]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"bb8cb07f479a2ede43add7bff010827df682fb86"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/exercises.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/exercises.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":17},"end":{"line":2,"column":46}},"2":{"start":{"line":4,"column":15},"end":{"line":4,"column":31}},"3":{"start":{"line":7,"column":0},"end":{"line":31,"column":3}},"4":{"start":{"line":8,"column":44},"end":{"line":8,"column":53}},"5":{"start":{"line":10,"column":14},"end":{"line":10,"column":16}},"6":{"start":{"line":11,"column":2},"end":{"line":20,"column":3}},"7":{"start":{"line":12,"column":4},"end":{"line":19,"column":6}},"8":{"start":{"line":22,"column":22},"end":{"line":29,"column":6}},"9":{"start":{"line":30,"column":4},"end":{"line":30,"column":36}},"10":{"start":{"line":33,"column":0},"end":{"line":33,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":7,"column":16},"end":{"line":7,"column":17}},"loc":{"start":{"line":7,"column":36},"end":{"line":31,"column":1}},"line":7}},"branchMap":{"0":{"loc":{"start":{"line":8,"column":18},"end":{"line":8,"column":26}},"type":"default-arg","locations":[{"start":{"line":8,"column":25},"end":{"line":8,"column":26}}],"line":8},"1":{"loc":{"start":{"line":8,"column":28},"end":{"line":8,"column":39}},"type":"default-arg","locations":[{"start":{"line":8,"column":36},"end":{"line":8,"column":39}}],"line":8},"2":{"loc":{"start":{"line":11,"column":2},"end":{"line":20,"column":3}},"type":"if","locations":[{"start":{"line":11,"column":2},"end":{"line":20,"column":3}},{"start":{},"end":{}}],"line":11}},"s":{"0":6,"1":6,"2":6,"3":6,"4":3,"5":3,"6":3,"7":1,"8":3,"9":3,"10":6},"f":{"0":3},"b":{"0":[2],"1":[2],"2":[1,2]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"c11eca912e5fdd851e2d51b3878275f9076d28da"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/splits.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/splits.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":14},"end":{"line":2,"column":40}},"2":{"start":{"line":3,"column":17},"end":{"line":3,"column":46}},"3":{"start":{"line":4,"column":20},"end":{"line":4,"column":59}},"4":{"start":{"line":6,"column":15},"end":{"line":6,"column":31}},"5":{"start":{"line":9,"column":0},"end":{"line":31,"column":3}},"6":{"start":{"line":10,"column":29},"end":{"line":10,"column":37}},"7":{"start":{"line":12,"column":2},"end":{"line":14,"column":3}},"8":{"start":{"line":13,"column":4},"end":{"line":13,"column":71}},"9":{"start":{"line":16,"column":2},"end":{"line":18,"column":3}},"10":{"start":{"line":17,"column":4},"end":{"line":17,"column":74}},"11":{"start":{"line":20,"column":21},"end":{"line":24,"column":6}},"12":{"start":{"line":26,"column":4},"end":{"line":26,"column":26}},"13":{"start":{"line":28,"column":27},"end":{"line":28,"column":82}},"14":{"start":{"line":30,"column":4},"end":{"line":30,"column":41}},"15":{"start":{"line":34,"column":0},"end":{"line":38,"column":3}},"16":{"start":{"line":35,"column":19},"end":{"line":35,"column":82}},"17":{"start":{"line":36,"column":4},"end":{"line":36,"column":33}},"18":{"start":{"line":41,"column":0},"end":{"line":64,"column":3}},"19":{"start":{"line":42,"column":18},"end":{"line":45,"column":27}},"20":{"start":{"line":47,"column":4},"end":{"line":49,"column":5}},"21":{"start":{"line":48,"column":6},"end":{"line":48,"column":66}},"22":{"start":{"line":51,"column":21},"end":{"line":51,"column":37}},"23":{"start":{"line":52,"column":24},"end":{"line":52,"column":75}},"24":{"start":{"line":52,"column":57},"end":{"line":52,"column":74}},"25":{"start":{"line":54,"column":4},"end":{"line":61,"column":5}},"26":{"start":{"line":55,"column":24},"end":{"line":55,"column":73}},"27":{"start":{"line":56,"column":19},"end":{"line":56,"column":59}},"28":{"start":{"line":56,"column":48},"end":{"line":56,"column":57}},"29":{"start":{"line":57,"column":6},"end":{"line":60,"column":10}},"30":{"start":{"line":57,"column":56},"end":{"line":60,"column":7}},"31":{"start":{"line":59,"column":51},"end":{"line":59,"column":63}},"32":{"start":{"line":63,"column":4},"end":{"line":63,"column":35}},"33":{"start":{"line":67,"column":0},"end":{"line":78,"column":3}},"34":{"start":{"line":68,"column":18},"end":{"line":71,"column":27}},"35":{"start":{"line":73,"column":4},"end":{"line":75,"column":5}},"36":{"start":{"line":74,"column":6},"end":{"line":74,"column":66}},"37":{"start":{"line":77,"column":4},"end":{"line":77,"column":32}},"38":{"start":{"line":81,"column":0},"end":{"line":113,"column":3}},"39":{"start":{"line":82,"column":29},"end":{"line":82,"column":37}},"40":{"start":{"line":84,"column":2},"end":{"line":86,"column":3}},"41":{"start":{"line":85,"column":4},"end":{"line":85,"column":71}},"42":{"start":{"line":88,"column":2},"end":{"line":90,"column":3}},"43":{"start":{"line":89,"column":4},"end":{"line":89,"column":74}},"44":{"start":{"line":92,"column":18},"end":{"line":105,"column":26}},"45":{"start":{"line":107,"column":4},"end":{"line":109,"column":5}},"46":{"start":{"line":108,"column":6},"end":{"line":108,"column":66}},"47":{"start":{"line":111,"column":4},"end":{"line":111,"column":32}},"48":{"start":{"line":116,"column":0},"end":{"line":127,"column":3}},"49":{"start":{"line":117,"column":18},"end":{"line":120,"column":6}},"50":{"start":{"line":122,"column":4},"end":{"line":124,"column":5}},"51":{"start":{"line":123,"column":6},"end":{"line":123,"column":66}},"52":{"start":{"line":126,"column":4},"end":{"line":126,"column":68}},"53":{"start":{"line":129,"column":0},"end":{"line":129,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":26},"end":{"line":9,"column":27}},"loc":{"start":{"line":9,"column":46},"end":{"line":31,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":34,"column":25},"end":{"line":34,"column":26}},"loc":{"start":{"line":34,"column":45},"end":{"line":38,"column":1}},"line":34},"2":{"name":"(anonymous_2)","decl":{"start":{"line":41,"column":36},"end":{"line":41,"column":37}},"loc":{"start":{"line":41,"column":56},"end":{"line":64,"column":1}},"line":41},"3":{"name":"(anonymous_3)","decl":{"start":{"line":52,"column":50},"end":{"line":52,"column":51}},"loc":{"start":{"line":52,"column":57},"end":{"line":52,"column":74}},"line":52},"4":{"name":"(anonymous_4)","decl":{"start":{"line":56,"column":41},"end":{"line":56,"column":42}},"loc":{"start":{"line":56,"column":48},"end":{"line":56,"column":57}},"line":56},"5":{"name":"(anonymous_5)","decl":{"start":{"line":57,"column":48},"end":{"line":57,"column":49}},"loc":{"start":{"line":57,"column":56},"end":{"line":60,"column":7}},"line":57},"6":{"name":"(anonymous_6)","decl":{"start":{"line":59,"column":43},"end":{"line":59,"column":44}},"loc":{"start":{"line":59,"column":51},"end":{"line":59,"column":63}},"line":59},"7":{"name":"(anonymous_7)","decl":{"start":{"line":67,"column":28},"end":{"line":67,"column":29}},"loc":{"start":{"line":67,"column":48},"end":{"line":78,"column":1}},"line":67},"8":{"name":"(anonymous_8)","decl":{"start":{"line":81,"column":28},"end":{"line":81,"column":29}},"loc":{"start":{"line":81,"column":48},"end":{"line":113,"column":1}},"line":81},"9":{"name":"(anonymous_9)","decl":{"start":{"line":116,"column":31},"end":{"line":116,"column":32}},"loc":{"start":{"line":116,"column":51},"end":{"line":127,"column":1}},"line":116}},"branchMap":{"0":{"loc":{"start":{"line":12,"column":2},"end":{"line":14,"column":3}},"type":"if","locations":[{"start":{"line":12,"column":2},"end":{"line":14,"column":3}},{"start":{},"end":{}}],"line":12},"1":{"loc":{"start":{"line":12,"column":6},"end":{"line":12,"column":27}},"type":"binary-expr","locations":[{"start":{"line":12,"column":6},"end":{"line":12,"column":11}},{"start":{"line":12,"column":15},"end":{"line":12,"column":27}}],"line":12},"2":{"loc":{"start":{"line":16,"column":2},"end":{"line":18,"column":3}},"type":"if","locations":[{"start":{"line":16,"column":2},"end":{"line":18,"column":3}},{"start":{},"end":{}}],"line":16},"3":{"loc":{"start":{"line":47,"column":4},"end":{"line":49,"column":5}},"type":"if","locations":[{"start":{"line":47,"column":4},"end":{"line":49,"column":5}},{"start":{},"end":{}}],"line":47},"4":{"loc":{"start":{"line":52,"column":57},"end":{"line":52,"column":74}},"type":"binary-expr","locations":[{"start":{"line":52,"column":57},"end":{"line":52,"column":68}},{"start":{"line":52,"column":72},"end":{"line":52,"column":74}}],"line":52},"5":{"loc":{"start":{"line":54,"column":4},"end":{"line":61,"column":5}},"type":"if","locations":[{"start":{"line":54,"column":4},"end":{"line":61,"column":5}},{"start":{},"end":{}}],"line":54},"6":{"loc":{"start":{"line":59,"column":20},"end":{"line":59,"column":37}},"type":"binary-expr","locations":[{"start":{"line":59,"column":20},"end":{"line":59,"column":31}},{"start":{"line":59,"column":35},"end":{"line":59,"column":37}}],"line":59},"7":{"loc":{"start":{"line":73,"column":4},"end":{"line":75,"column":5}},"type":"if","locations":[{"start":{"line":73,"column":4},"end":{"line":75,"column":5}},{"start":{},"end":{}}],"line":73},"8":{"loc":{"start":{"line":84,"column":2},"end":{"line":86,"column":3}},"type":"if","locations":[{"start":{"line":84,"column":2},"end":{"line":86,"column":3}},{"start":{},"end":{}}],"line":84},"9":{"loc":{"start":{"line":84,"column":6},"end":{"line":84,"column":27}},"type":"binary-expr","locations":[{"start":{"line":84,"column":6},"end":{"line":84,"column":11}},{"start":{"line":84,"column":15},"end":{"line":84,"column":27}}],"line":84},"10":{"loc":{"start":{"line":88,"column":2},"end":{"line":90,"column":3}},"type":"if","locations":[{"start":{"line":88,"column":2},"end":{"line":90,"column":3}},{"start":{},"end":{}}],"line":88},"11":{"loc":{"start":{"line":107,"column":4},"end":{"line":109,"column":5}},"type":"if","locations":[{"start":{"line":107,"column":4},"end":{"line":109,"column":5}},{"start":{},"end":{}}],"line":107},"12":{"loc":{"start":{"line":122,"column":4},"end":{"line":124,"column":5}},"type":"if","locations":[{"start":{"line":122,"column":4},"end":{"line":124,"column":5}},{"start":{},"end":{}}],"line":122}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":12,"7":12,"8":1,"9":11,"10":1,"11":10,"12":10,"13":10,"14":10,"15":6,"16":1,"17":1,"18":6,"19":2,"20":2,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"31":1,"32":1,"33":6,"34":3,"35":3,"36":2,"37":1,"38":6,"39":4,"40":4,"41":1,"42":3,"43":1,"44":2,"45":2,"46":1,"47":1,"48":6,"49":2,"50":2,"51":1,"52":1,"53":6},"f":{"0":12,"1":1,"2":2,"3":1,"4":1,"5":1,"6":1,"7":3,"8":4,"9":2},"b":{"0":[1,11],"1":[12,11],"2":[1,10],"3":[1,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[2,1],"8":[1,3],"9":[4,3],"10":[1,2],"11":[1,1],"12":[1,1]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"3f9beca440eb5cba6eac9c0fdba1c15b2985f866"} +,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/workouts.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/workouts.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":17},"end":{"line":2,"column":46}},"2":{"start":{"line":3,"column":16},"end":{"line":3,"column":44}},"3":{"start":{"line":4,"column":20},"end":{"line":4,"column":59}},"4":{"start":{"line":6,"column":15},"end":{"line":6,"column":31}},"5":{"start":{"line":9,"column":0},"end":{"line":30,"column":3}},"6":{"start":{"line":10,"column":30},"end":{"line":10,"column":38}},"7":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"8":{"start":{"line":14,"column":4},"end":{"line":14,"column":73}},"9":{"start":{"line":17,"column":2},"end":{"line":19,"column":3}},"10":{"start":{"line":18,"column":4},"end":{"line":18,"column":84}},"11":{"start":{"line":22,"column":20},"end":{"line":26,"column":6}},"12":{"start":{"line":27,"column":4},"end":{"line":27,"column":25}},"13":{"start":{"line":28,"column":4},"end":{"line":28,"column":34}},"14":{"start":{"line":33,"column":0},"end":{"line":50,"column":3}},"15":{"start":{"line":34,"column":21},"end":{"line":34,"column":65}},"16":{"start":{"line":37,"column":30},"end":{"line":47,"column":5}},"17":{"start":{"line":39,"column":26},"end":{"line":41,"column":10}},"18":{"start":{"line":42,"column":8},"end":{"line":45,"column":10}},"19":{"start":{"line":49,"column":4},"end":{"line":49,"column":44}},"20":{"start":{"line":53,"column":0},"end":{"line":64,"column":3}},"21":{"start":{"line":55,"column":20},"end":{"line":58,"column":6}},"22":{"start":{"line":60,"column":4},"end":{"line":62,"column":5}},"23":{"start":{"line":61,"column":6},"end":{"line":61,"column":68}},"24":{"start":{"line":63,"column":4},"end":{"line":63,"column":34}},"25":{"start":{"line":67,"column":0},"end":{"line":99,"column":3}},"26":{"start":{"line":68,"column":30},"end":{"line":68,"column":38}},"27":{"start":{"line":70,"column":2},"end":{"line":72,"column":3}},"28":{"start":{"line":71,"column":4},"end":{"line":71,"column":73}},"29":{"start":{"line":74,"column":2},"end":{"line":76,"column":3}},"30":{"start":{"line":75,"column":4},"end":{"line":75,"column":84}},"31":{"start":{"line":79,"column":20},"end":{"line":92,"column":5}},"32":{"start":{"line":94,"column":4},"end":{"line":96,"column":5}},"33":{"start":{"line":95,"column":6},"end":{"line":95,"column":68}},"34":{"start":{"line":98,"column":4},"end":{"line":98,"column":34}},"35":{"start":{"line":102,"column":0},"end":{"line":115,"column":3}},"36":{"start":{"line":104,"column":20},"end":{"line":107,"column":6}},"37":{"start":{"line":109,"column":4},"end":{"line":111,"column":5}},"38":{"start":{"line":110,"column":6},"end":{"line":110,"column":68}},"39":{"start":{"line":113,"column":4},"end":{"line":113,"column":70}},"40":{"start":{"line":117,"column":0},"end":{"line":117,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":26},"end":{"line":9,"column":27}},"loc":{"start":{"line":9,"column":46},"end":{"line":30,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":33,"column":25},"end":{"line":33,"column":26}},"loc":{"start":{"line":33,"column":45},"end":{"line":50,"column":1}},"line":33},"2":{"name":"(anonymous_2)","decl":{"start":{"line":38,"column":19},"end":{"line":38,"column":20}},"loc":{"start":{"line":38,"column":38},"end":{"line":46,"column":7}},"line":38},"3":{"name":"(anonymous_3)","decl":{"start":{"line":53,"column":28},"end":{"line":53,"column":29}},"loc":{"start":{"line":53,"column":48},"end":{"line":64,"column":1}},"line":53},"4":{"name":"(anonymous_4)","decl":{"start":{"line":67,"column":28},"end":{"line":67,"column":29}},"loc":{"start":{"line":67,"column":48},"end":{"line":99,"column":1}},"line":67},"5":{"name":"(anonymous_5)","decl":{"start":{"line":102,"column":31},"end":{"line":102,"column":32}},"loc":{"start":{"line":102,"column":51},"end":{"line":115,"column":1}},"line":102}},"branchMap":{"0":{"loc":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"type":"if","locations":[{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},{"start":{},"end":{}}],"line":13},"1":{"loc":{"start":{"line":13,"column":6},"end":{"line":13,"column":27}},"type":"binary-expr","locations":[{"start":{"line":13,"column":6},"end":{"line":13,"column":11}},{"start":{"line":13,"column":15},"end":{"line":13,"column":27}}],"line":13},"2":{"loc":{"start":{"line":17,"column":2},"end":{"line":19,"column":3}},"type":"if","locations":[{"start":{"line":17,"column":2},"end":{"line":19,"column":3}},{"start":{},"end":{}}],"line":17},"3":{"loc":{"start":{"line":17,"column":6},"end":{"line":17,"column":57}},"type":"binary-expr","locations":[{"start":{"line":17,"column":6},"end":{"line":17,"column":31}},{"start":{"line":17,"column":35},"end":{"line":17,"column":57}}],"line":17},"4":{"loc":{"start":{"line":60,"column":4},"end":{"line":62,"column":5}},"type":"if","locations":[{"start":{"line":60,"column":4},"end":{"line":62,"column":5}},{"start":{},"end":{}}],"line":60},"5":{"loc":{"start":{"line":70,"column":2},"end":{"line":72,"column":3}},"type":"if","locations":[{"start":{"line":70,"column":2},"end":{"line":72,"column":3}},{"start":{},"end":{}}],"line":70},"6":{"loc":{"start":{"line":70,"column":6},"end":{"line":70,"column":27}},"type":"binary-expr","locations":[{"start":{"line":70,"column":6},"end":{"line":70,"column":11}},{"start":{"line":70,"column":15},"end":{"line":70,"column":27}}],"line":70},"7":{"loc":{"start":{"line":74,"column":2},"end":{"line":76,"column":3}},"type":"if","locations":[{"start":{"line":74,"column":2},"end":{"line":76,"column":3}},{"start":{},"end":{}}],"line":74},"8":{"loc":{"start":{"line":74,"column":6},"end":{"line":74,"column":57}},"type":"binary-expr","locations":[{"start":{"line":74,"column":6},"end":{"line":74,"column":31}},{"start":{"line":74,"column":35},"end":{"line":74,"column":57}}],"line":74},"9":{"loc":{"start":{"line":94,"column":4},"end":{"line":96,"column":5}},"type":"if","locations":[{"start":{"line":94,"column":4},"end":{"line":96,"column":5}},{"start":{},"end":{}}],"line":94},"10":{"loc":{"start":{"line":109,"column":4},"end":{"line":111,"column":5}},"type":"if","locations":[{"start":{"line":109,"column":4},"end":{"line":111,"column":5}},{"start":{},"end":{}}],"line":109}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":15,"7":15,"8":1,"9":14,"10":2,"11":12,"12":12,"13":12,"14":6,"15":1,"16":1,"17":1,"18":1,"19":1,"20":6,"21":6,"22":5,"23":3,"24":2,"25":6,"26":4,"27":4,"28":1,"29":3,"30":1,"31":2,"32":2,"33":1,"34":1,"35":6,"36":2,"37":2,"38":1,"39":1,"40":6},"f":{"0":15,"1":1,"2":1,"3":6,"4":4,"5":2},"b":{"0":[1,14],"1":[15,14],"2":[2,12],"3":[14,13],"4":[3,2],"5":[1,3],"6":[4,3],"7":[1,2],"8":[3,3],"9":[1,1],"10":[1,1]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"4e01a578f1f069b10bd102e6b3ed8954e91e8781"} +} diff --git a/backend/coverage/lcov-report/backend/app.js.html b/backend/coverage/lcov-report/backend/app.js.html new file mode 100644 index 0000000..036a6bf --- /dev/null +++ b/backend/coverage/lcov-report/backend/app.js.html @@ -0,0 +1,229 @@ + + + + + + Code coverage report for backend/app.js + + + + + + + + + +
+
+

All files / backend app.js

+
+ +
+ 100% + Statements + 33/33 +
+ + +
+ 100% + Branches + 6/6 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 32/32 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +496x +6x +6x +6x +6x +6x +  +6x +6x +6x +6x +6x +6x +  +6x +  +6x +  +6x +  +  +6x +6x +  +  +143x +1x +  +  +  +6x +  +6x +6x +6x +6x +6x +6x +  +  +6x +6x +1x +  +  +6x +6x +  +6x
const express = require("express");
+const cors = require("cors");
+const helmet = require("helmet");
+const bodyParser = require("body-parser");
+const path = require("path");
+require("express-async-errors");
+ 
+const exerciseRoutes = require("./routes/exercises");
+const workoutRoutes = require("./routes/workouts");
+const splitRoutes = require("./routes/splits");
+const authRoutes = require("./routes/auth");
+const aiRoutes = require("./routes/ai");
+const { apiLimiter, authLimiter, aiLimiter } = require("./middleware/rateLimiters");
+ 
+const app = express();
+ 
+const allowedOrigins = (process.env.CORS_ORIGIN || "http://localhost:3000")
+  .split(",")
+  .map((origin) => origin.trim())
+  .filter(Boolean);
+ 
+app.use(helmet({ contentSecurityPolicy: false }));
+app.use(
+  cors({
+    origin: (origin, callback) => {
+      if (!origin || allowedOrigins.includes(origin)) return callback(null, true);
+      return callback(new Error("Not allowed by CORS"));
+    },
+  })
+);
+app.use(bodyParser.json({ limit: "1mb" }));
+ 
+app.use("/api", apiLimiter);
+app.use("/api/exercises", exerciseRoutes);
+app.use("/api/workouts", workoutRoutes);
+app.use("/api/splits", splitRoutes);
+app.use("/api/auth", authLimiter, authRoutes);
+app.use("/api/ai", aiLimiter, aiRoutes);
+ 
+// Serve the React build + SPA catchall
+app.use(express.static(path.join(__dirname, "../frontend/build")));
+app.get("*", (req, res) => {
+  res.sendFile(path.join(__dirname, "../frontend/build", "index.html"));
+});
+ 
+const errorHandler = require("./middleware/errorHandler");
+app.use(errorHandler);
+ 
+module.exports = app;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/index.html b/backend/coverage/lcov-report/backend/index.html new file mode 100644 index 0000000..d506997 --- /dev/null +++ b/backend/coverage/lcov-report/backend/index.html @@ -0,0 +1,116 @@ + + + + + + Code coverage report for backend + + + + + + + + + +
+
+

All files backend

+
+ +
+ 100% + Statements + 33/33 +
+ + +
+ 100% + Branches + 6/6 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 32/32 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
app.js +
+
100%33/33100%6/6100%3/3100%32/32
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/middleware/authMiddleware.js.html b/backend/coverage/lcov-report/backend/middleware/authMiddleware.js.html new file mode 100644 index 0000000..8298956 --- /dev/null +++ b/backend/coverage/lcov-report/backend/middleware/authMiddleware.js.html @@ -0,0 +1,151 @@ + + + + + + Code coverage report for backend/middleware/authMiddleware.js + + + + + + + + + +
+
+

All files / backend/middleware authMiddleware.js

+
+ +
+ 100% + Statements + 16/16 +
+ + +
+ 100% + Branches + 6/6 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 16/16 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +236x +6x +  +6x +76x +76x +76x +8x +  +68x +68x +66x +66x +1x +  +65x +65x +  +2x +  +  +  +6x
const jwt = require("jsonwebtoken");
+const User = require("../models/User");
+ 
+const protect = async (req, res, next) => {
+  try {
+    const authHeader = req.headers.authorization;
+    if (!authHeader || !authHeader.startsWith("Bearer ")) {
+      return res.status(401).json({ message: "Not authorized, no token" });
+    }
+    const token = authHeader.split(" ")[1];
+    const decoded = jwt.verify(token, process.env.JWT_SECRET);
+    const user = await User.findById(decoded.userId).select("-passwordHash");
+    if (!user) {
+      return res.status(401).json({ message: "Not authorized, user not found" });
+    }
+    req.user = user;
+    next();
+  } catch (error) {
+    return res.status(401).json({ message: "Not authorized, token failed" });
+  }
+};
+ 
+module.exports = { protect };
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/middleware/errorHandler.js.html b/backend/coverage/lcov-report/backend/middleware/errorHandler.js.html new file mode 100644 index 0000000..e80dbd3 --- /dev/null +++ b/backend/coverage/lcov-report/backend/middleware/errorHandler.js.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for backend/middleware/errorHandler.js + + + + + + + + + +
+
+

All files / backend/middleware errorHandler.js

+
+ +
+ 100% + Statements + 8/8 +
+ + +
+ 100% + Branches + 4/4 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 8/8 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13  +7x +7x +2x +  +5x +1x +  +4x +4x +  +  +7x
// Central error handler — must be the LAST app.use().
+const errorHandler = (err, req, res, next) => {
+  if (err.name === "CastError") {
+    return res.status(400).json({ message: "Invalid ID format" });   // bad ObjectId
+  }
+  if (err.name === "ValidationError") {
+    return res.status(400).json({ message: err.message });           // mongoose validation
+  }
+  console.error("Unhandled error:", err.message);
+  res.status(500).json({ message: "Server Error" });
+};
+ 
+module.exports = errorHandler;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/middleware/index.html b/backend/coverage/lcov-report/backend/middleware/index.html new file mode 100644 index 0000000..e89c315 --- /dev/null +++ b/backend/coverage/lcov-report/backend/middleware/index.html @@ -0,0 +1,146 @@ + + + + + + Code coverage report for backend/middleware + + + + + + + + + +
+
+

All files backend/middleware

+
+ +
+ 100% + Statements + 31/31 +
+ + +
+ 100% + Branches + 10/10 +
+ + +
+ 100% + Functions + 3/3 +
+ + +
+ 100% + Lines + 30/30 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
authMiddleware.js +
+
100%16/16100%6/6100%1/1100%16/16
errorHandler.js +
+
100%8/8100%4/4100%1/1100%8/8
rateLimiters.js +
+
100%7/7100%0/0100%1/1100%6/6
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/middleware/rateLimiters.js.html b/backend/coverage/lcov-report/backend/middleware/rateLimiters.js.html new file mode 100644 index 0000000..c9906f5 --- /dev/null +++ b/backend/coverage/lcov-report/backend/middleware/rateLimiters.js.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for backend/middleware/rateLimiters.js + + + + + + + + + +
+
+

All files / backend/middleware rateLimiters.js

+
+ +
+ 100% + Statements + 7/7 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 6/6 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +336x +  +  +223x +  +6x +  +  +  +  +  +  +  +  +6x +  +  +  +  +  +  +  +  +6x +  +  +  +  +  +  +  +  +6x
const rateLimit = require("express-rate-limit");
+ 
+// Disable rate limiting during tests so the suite isn't throttled.
+const skip = () => process.env.NODE_ENV === "test";
+ 
+const apiLimiter = rateLimit({
+  windowMs: 15 * 60 * 1000,
+  max: 300,
+  standardHeaders: true,
+  legacyHeaders: false,
+  skip,
+  message: { message: "Too many requests, please try again later." },
+});
+ 
+const authLimiter = rateLimit({
+  windowMs: 15 * 60 * 1000,
+  max: 20,
+  standardHeaders: true,
+  legacyHeaders: false,
+  skip,
+  message: { message: "Too many authentication attempts, please try again later." },
+});
+ 
+const aiLimiter = rateLimit({
+  windowMs: 60 * 60 * 1000,
+  max: 30,
+  standardHeaders: true,
+  legacyHeaders: false,
+  skip,
+  message: { message: "AI generation limit reached, please try again later." },
+});
+ 
+module.exports = { apiLimiter, authLimiter, aiLimiter };
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/Exercise.js.html b/backend/coverage/lcov-report/backend/models/Exercise.js.html new file mode 100644 index 0000000..854d40f --- /dev/null +++ b/backend/coverage/lcov-report/backend/models/Exercise.js.html @@ -0,0 +1,121 @@ + + + + + + Code coverage report for backend/models/Exercise.js + + + + + + + + + +
+
+

All files / backend/models Exercise.js

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +136x +  +6x +  +  +  +  +  +  +  +  +6x + 
const mongoose = require("mongoose");
+ 
+const exerciseSchema = new mongoose.Schema({
+  id: Number,
+  name: String,
+  bodyPart: String,
+  equipment: String,
+  target: String,
+  gifUrl: String,
+});
+ 
+module.exports = mongoose.model("Exercise", exerciseSchema);
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/Split.js.html b/backend/coverage/lcov-report/backend/models/Split.js.html new file mode 100644 index 0000000..493158f --- /dev/null +++ b/backend/coverage/lcov-report/backend/models/Split.js.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for backend/models/Split.js + + + + + + + + + +
+
+

All files / backend/models Split.js

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +266x +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x + 
const mongoose = require("mongoose");
+ 
+const splitSchema = new mongoose.Schema({
+  userId: {
+    type: mongoose.Schema.Types.ObjectId,
+    ref: "User",
+    required: true,
+    index: true,
+  },
+  name: {
+    type: String,
+    required: true,
+    trim: true,
+  },
+  workouts: [
+    {
+      type: mongoose.Schema.Types.ObjectId,
+      ref: "Workout",
+    }
+  ],
+},
+  { timestamps: true }
+);
+ 
+module.exports = mongoose.model("Split", splitSchema);
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/User.js.html b/backend/coverage/lcov-report/backend/models/User.js.html new file mode 100644 index 0000000..cb59ad6 --- /dev/null +++ b/backend/coverage/lcov-report/backend/models/User.js.html @@ -0,0 +1,241 @@ + + + + + + Code coverage report for backend/models/User.js + + + + + + + + + +
+
+

All files / backend/models User.js

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +536x +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x
const mongoose = require("mongoose");
+ 
+const userSchema = new mongoose.Schema(
+    {
+        name: {
+            type: String,
+            required: true,
+            trim: true
+        },
+        email: {
+            type: String,
+            required: true,
+            unique: true,
+            lowercase: true,
+            trim: true,
+        },
+        passwordHash: {
+            type: String,
+            required: true,
+        },
+        fitnessProfile: {
+            goal: {
+                type: String,
+                enum: ["strength", "hypertrophy", "fat_loss", "general_fitness"],
+                default: "general_fitness",
+            },
+            experienceLevel: {
+                type: String,
+                enum: ["beginner", "intermediate", "advanced"],
+                default: "beginner",
+            },
+            daysPerWeek: {
+                type: Number,
+                default: 3,
+            },
+            sessionLengthMinutes: {
+                type: Number,
+                default: 60,
+            },
+            equipment: {
+                type: [String],
+                default: [],
+            },
+            limitations: {
+                type: [String],
+                default: [],
+            },
+        },
+    },
+    {timestamps: true}
+);
+ 
+module.exports = mongoose.model("User", userSchema);
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/Workout.js.html b/backend/coverage/lcov-report/backend/models/Workout.js.html new file mode 100644 index 0000000..97f5c8a --- /dev/null +++ b/backend/coverage/lcov-report/backend/models/Workout.js.html @@ -0,0 +1,160 @@ + + + + + + Code coverage report for backend/models/Workout.js + + + + + + + + + +
+
+

All files / backend/models Workout.js

+
+ +
+ 100% + Statements + 3/3 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 3/3 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +266x +  +6x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x + 
const mongoose = require("mongoose");
+ 
+const workoutSchema = new mongoose.Schema(
+  {
+    userId: {
+      type: mongoose.Schema.Types.ObjectId,
+      ref: "User",
+      required: true,
+      index: true,
+    },
+    name: {
+      type: String,
+      required: true,
+      trim: true,
+    },
+    exercises: [
+      {
+        type: Number, 
+      },
+    ],
+  },
+  { timestamps: true }
+);
+ 
+module.exports = mongoose.model("Workout", workoutSchema);
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/index.html b/backend/coverage/lcov-report/backend/models/index.html new file mode 100644 index 0000000..9c1594d --- /dev/null +++ b/backend/coverage/lcov-report/backend/models/index.html @@ -0,0 +1,161 @@ + + + + + + Code coverage report for backend/models + + + + + + + + + +
+
+

All files backend/models

+
+ +
+ 100% + Statements + 12/12 +
+ + +
+ 100% + Branches + 0/0 +
+ + +
+ 100% + Functions + 0/0 +
+ + +
+ 100% + Lines + 12/12 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
Exercise.js +
+
100%3/3100%0/0100%0/0100%3/3
Split.js +
+
100%3/3100%0/0100%0/0100%3/3
User.js +
+
100%3/3100%0/0100%0/0100%3/3
Workout.js +
+
100%3/3100%0/0100%0/0100%3/3
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/ai.js.html b/backend/coverage/lcov-report/backend/routes/ai.js.html new file mode 100644 index 0000000..f693391 --- /dev/null +++ b/backend/coverage/lcov-report/backend/routes/ai.js.html @@ -0,0 +1,724 @@ + + + + + + Code coverage report for backend/routes/ai.js + + + + + + + + + +
+
+

All files / backend/routes ai.js

+
+ +
+ 100% + Statements + 58/58 +
+ + +
+ 68.18% + Branches + 30/44 +
+ + +
+ 100% + Functions + 8/8 +
+ + +
+ 100% + Lines + 55/55 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +2146x +6x +6x +6x +6x +  +6x +  +6x +4x +4x +  +4x +1x +  +  +3x +  +  +6x +8x +  +  +  +  +  +  +  +  +8x +  +  +8x +  +  +1x +  +  +  +  +8x +  +  +  +8x +1x +  +  +  +  +7x +  +  +  +  +  +  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +7x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +6x +  +6x +1x +1x +1x +1x +1x +  +1x +  +  +  +  +  +  +  +  +  +5x +  +5x +1x +  +  +  +  +4x +  +3x +  +3x +3x +3x +1x +  +  +  +  +  +  +  +2x +  +  +  +  +6x +2x +  +2x +1x +  +  +1x +  +1x +1x +  +1x +  +  +  +  +  +1x +  +  +1x +  +  +1x +  +  +1x +  +  +  +  +  +  +6x
const express = require("express");
+const Exercise = require("../models/Exercise");
+const { protect } = require("../middleware/authMiddleware");
+const Workout = require("../models/Workout");
+const Split = require("../models/Split");
+ 
+const router = express.Router();
+ 
+const extractJson = (text) => {
+  const firstBrace = text.indexOf("{");
+  const lastBrace = text.lastIndexOf("}");
+ 
+  if (firstBrace === -1 || lastBrace === -1) {
+    throw new Error("LLM response did not contain JSON");
+  }
+ 
+  return JSON.parse(text.slice(firstBrace, lastBrace + 1));
+};
+ 
+router.post("/generate-workout", protect, async (req, res) => {
+  const profile = req.user.fitnessProfile || {};
+ 
+  const {
+    goal = "general_fitness",
+    experienceLevel = "beginner",
+    daysPerWeek = 3,
+    sessionLengthMinutes = 60,
+    equipment = [],
+    limitations = [],
+  } = profile;
+ 
+  const equipmentFilter =
+    equipment.length > 0
+      ? {
+          equipment: {
+            $in: equipment.map((item) => new RegExp(item, "i")),
+          },
+        }
+      : {};
+ 
+  const exercises = await Exercise.find(equipmentFilter)
+    .limit(40)
+    .select("id name bodyPart equipment target");
+ 
+  if (exercises.length === 0) {
+    return res.status(400).json({
+      message: "No matching exercises found for this profile.",
+    });
+  }
+ 
+  const exerciseOptions = exercises.map((exercise) => ({
+    id: exercise.id,
+    name: exercise.name,
+    bodyPart: exercise.bodyPart,
+    equipment: exercise.equipment,
+    target: exercise.target,
+  }));
+ 
+  const systemPrompt = `
+You are FitForge AI, a workout planning assistant.
+ 
+Return ONLY valid JSON.
+Do not use markdown.
+Do not wrap the response in triple backticks.
+Do not include comments.
+Do not include trailing commas.
+Use double quotes for all JSON keys and string values.
+Generate safe, structured workout plans using ONLY the exercises provided.
+Do not invent exercise IDs.
+Do not provide medical advice.
+If limitations are listed, avoid exercises that obviously conflict with them.
+`;
+ 
+  const userPrompt = `
+Create a personalized workout plan.
+ 
+User profile:
+- Goal: ${goal}
+- Experience level: ${experienceLevel}
+- Days per week: ${daysPerWeek}
+- Session length minutes: ${sessionLengthMinutes}
+- Equipment: ${equipment.join(", ") || "not specified"}
+- Limitations: ${limitations.join(", ") || "none"}
+ 
+Available exercises:
+${JSON.stringify(exerciseOptions, null, 2)}
+ 
+Return JSON in this exact shape:
+{
+  "programName": "string",
+  "goal": "string",
+  "daysPerWeek": number,
+  "days": [
+    {
+      "day": 1,
+      "name": "string",
+      "focus": "string",
+      "exercises": [
+        {
+          "exerciseId": number,
+          "name": "string",
+          "sets": number,
+          "repRange": "string",
+          "restSeconds": number,
+          "reason": "string"
+        }
+      ]
+    }
+  ],
+  "notes": ["string"]
+}
+`;
+ 
+  const llmResponse = await fetch(process.env.LLM_API_URL, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+      Authorization: `Bearer ${process.env.LLM_API_KEY}`,
+    },
+    body: JSON.stringify({
+      model: process.env.LLM_MODEL,
+      messages: [
+        { role: "system", content: systemPrompt },
+        { role: "user", content: userPrompt },
+      ],
+      temperature: 0.2,
+      response_format: {
+        type: "json_object",
+      },
+    }),
+  });
+ 
+  const llmData = await llmResponse.json();
+ 
+  if (!llmResponse.ok) {
+    console.error("LLM status:", llmResponse.status);
+    console.error("Retry after:", llmResponse.headers.get("retry-after"));
+    console.error("Rate limit remaining requests:", llmResponse.headers.get("x-ratelimit-remaining-requests"));
+    console.error("Rate limit remaining tokens:", llmResponse.headers.get("x-ratelimit-remaining-tokens"));
+    console.error("LLM request failed:", JSON.stringify(llmData, null, 2));
+ 
+    return res.status(llmResponse.status === 429 ? 429 : 500).json({
+      message:
+        llmResponse.status === 429
+          ? "Rate limit hit. Please wait and try again."
+          : "LLM request failed",
+      retryAfter: llmResponse.headers.get("retry-after"),
+      error: llmData,
+    });
+  }
+ 
+  const content = llmData.choices?.[0]?.message?.content;
+ 
+  if (!content) {
+    return res.status(500).json({
+      message: "LLM returned no content",
+    });
+  }
+ 
+  const plan = extractJson(content);
+ 
+  const validExerciseIds = new Set(exerciseOptions.map((exercise) => exercise.id));
+ 
+  for (const day of plan.days || []) {
+    for (const exercise of day.exercises || []) {
+      if (!validExerciseIds.has(exercise.exerciseId)) {
+        return res.status(500).json({
+          message: "LLM returned an invalid exercise ID",
+          invalidExerciseId: exercise.exerciseId,
+        });
+      }
+    }
+  }
+ 
+  res.status(200).json({
+    plan,
+  });
+});
+ 
+router.post("/save-generated-plan", protect, async (req, res) => {
+  const { plan } = req.body;
+ 
+  if (!plan || !Array.isArray(plan.days)) {
+    return res.status(400).json({ message: "Valid plan is required" });
+  }
+ 
+  const createdWorkouts = [];
+ 
+  for (const day of plan.days) {
+    const exerciseIds = (day.exercises || []).map((exercise) => exercise.exerciseId);
+ 
+    const workout = await Workout.create({
+      userId: req.user._id,
+      name: day.name || `Day ${day.day}`,
+      exercises: exerciseIds,
+    });
+ 
+    createdWorkouts.push(workout);
+  }
+ 
+  const split = await Split.create({
+    userId: req.user._id,
+    name: plan.programName || "AI Generated Plan",
+    workouts: createdWorkouts.map((workout) => workout._id),
+  });
+ 
+  res.status(201).json({
+    message: "AI plan saved successfully",
+    split,
+    workouts: createdWorkouts,
+  });
+});
+ 
+module.exports = router;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/auth.js.html b/backend/coverage/lcov-report/backend/routes/auth.js.html new file mode 100644 index 0000000..3496fda --- /dev/null +++ b/backend/coverage/lcov-report/backend/routes/auth.js.html @@ -0,0 +1,436 @@ + + + + + + Code coverage report for backend/routes/auth.js + + + + + + + + + +
+
+

All files / backend/routes auth.js

+
+ +
+ 97.72% + Statements + 43/44 +
+ + +
+ 94.73% + Branches + 18/19 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 97.67% + Lines + 42/43 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +1186x +6x +6x +6x +6x +  +6x +  +6x +56x +  +  +  +  +59x +  +  +  +  +  +  +6x +58x +  +58x +1x +  +  +57x +1x +  +  +56x +  +56x +1x +  +  +55x +  +55x +  +  +  +  +  +55x +  +  +  +  +  +  +6x +4x +  +4x +1x +  +  +3x +  +3x +1x +  +  +2x +  +2x +1x +  +  +1x +  +  +  +  +  +6x +1x +  +  +  +  +6x +  +  +  +  +  +  +  +  +2x +  +2x +  +2x +  +  +  +2x +  +  +  +  +  +  +  +  +  +2x +  +2x +  +  +  +6x
const express = require("express");
+const bcrypt = require("bcryptjs");
+const jwt = require("jsonwebtoken");
+const User = require("../models/User");
+const { protect } = require("../middleware/authMiddleware");
+ 
+const router = express.Router();
+ 
+const createToken = (userId) => {
+    return jwt.sign({ userId }, process.env.JWT_SECRET, {
+        expiresIn: "7d",
+    });
+};
+ 
+const serializeUser = (user) => ({
+    id: user._id,
+    name: user.name,
+    email: user.email,
+    fitnessProfile: user.fitnessProfile,
+});
+ 
+router.post("/register", async (req, res) => {
+        const { name, email, password } = req.body;
+ 
+        if (!name || !email || !password) { 
+            return res.status(400).json({ message: "Name, email, and password are required" });
+        }
+ 
+        if (password.length < 8) { 
+            return res.status(400).json({ message: "Password must be at least 8 characters" });
+        }
+ 
+        const existingUser = await User.findOne({ email });
+ 
+        if (existingUser) {
+            return res.status(409).json({ message: "Email already registered" });
+        }
+ 
+        const passwordHash = await bcrypt.hash(password, 12);
+ 
+        const user = await User.create({
+            name,
+            email,
+            passwordHash,
+        });
+ 
+        res.status(201).json({
+            token: createToken(user._id),
+            user: serializeUser(user),
+        });
+ 
+});
+ 
+router.post("/login", async (req, res) => { 
+        const { email, password } = req.body;
+ 
+        if (!email || !password) {
+            return res.status(400).json({ message: "Email and password are required" });
+        }
+ 
+        const user = await User.findOne({ email });
+ 
+        if (!user) { 
+            return res.status(401).json({ message: "Invalid email or password" });
+        }
+ 
+        const passwordMatches = await bcrypt.compare(password, user.passwordHash);
+ 
+        if (!passwordMatches) { 
+            return res.status(401).json({ message: "Invalid email or password" });
+        }
+ 
+        res.status(200).json({
+            token: createToken(user._id),
+            user: serializeUser(user),
+        });
+});
+ 
+router.get("/me", protect, async (req, res) => {
+    res.status(200).json({
+        user: serializeUser(req.user),
+    });
+});
+ 
+router.put("/profile", protect, async (req, res) => {
+ 
+        const {
+            goal,
+            experienceLevel,
+            daysPerWeek,
+            sessionLengthMinutes,
+            equipment,
+            limitations,
+        } = req.body;
+ 
+        const user = await User.findById(req.user._id);
+ 
+        Iif (!user) {
+            return res.status(400).json({ message: "User not found" });
+        }
+ 
+        user.fitnessProfile = {
+            ...user.fitnessProfile,
+            goal,
+            experienceLevel,
+            daysPerWeek,
+            sessionLengthMinutes,
+            equipment,
+            limitations,
+        };
+ 
+        await user.save();
+ 
+        res.status(200).json({ user: serializeUser(user) });
+ 
+});
+ 
+module.exports = router;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/exercises.js.html b/backend/coverage/lcov-report/backend/routes/exercises.js.html new file mode 100644 index 0000000..9652311 --- /dev/null +++ b/backend/coverage/lcov-report/backend/routes/exercises.js.html @@ -0,0 +1,181 @@ + + + + + + Code coverage report for backend/routes/exercises.js + + + + + + + + + +
+
+

All files / backend/routes exercises.js

+
+ +
+ 100% + Statements + 11/11 +
+ + +
+ 100% + Branches + 4/4 +
+ + +
+ 100% + Functions + 1/1 +
+ + +
+ 100% + Lines + 11/11 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +336x +6x +  +6x +  +  +6x +3x +  +3x +3x +1x +  +  +  +  +  +  +  +  +  +3x +  +  +  +  +  +  +  +3x +  +  +6x
const express = require("express");
+const Exercise = require("../models/Exercise");
+ 
+const router = express.Router();
+ 
+// Get exercises with optional search and pagination
+router.get("/", async (req, res) => {
+  const { search, page = 1, limit = 100 } = req.query;
+ 
+  let query = {};
+  if (search) {
+    query = {
+      $or: [
+        { name: { $regex: search, $options: "i" } },
+        { bodyPart: { $regex: search, $options: "i" } },
+        { equipment: { $regex: search, $options: "i" } },
+        { target: { $regex: search, $options: "i" } },
+      ],
+    };
+  }
+ 
+    const exercises = await Exercise.aggregate([
+      { $match: query },
+      { $group: { _id: "$name", doc: { $first: "$$ROOT" } } },
+      { $replaceRoot: { newRoot: "$doc" } },
+      { $sort: { name: 1 } },
+      { $skip: (page - 1) * limit },
+      { $limit: parseInt(limit) },
+    ]);
+    res.status(200).json(exercises);
+});
+ 
+module.exports = router;
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/index.html b/backend/coverage/lcov-report/backend/routes/index.html new file mode 100644 index 0000000..4419116 --- /dev/null +++ b/backend/coverage/lcov-report/backend/routes/index.html @@ -0,0 +1,176 @@ + + + + + + Code coverage report for backend/routes + + + + + + + + + +
+
+

All files backend/routes

+
+ +
+ 99.51% + Statements + 207/208 +
+ + +
+ 84.34% + Branches + 97/115 +
+ + +
+ 100% + Functions + 31/31 +
+ + +
+ 99.5% + Lines + 200/201 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
ai.js +
+
100%58/5868.18%30/44100%8/8100%55/55
auth.js +
+
97.72%43/4494.73%18/19100%6/697.67%42/43
exercises.js +
+
100%11/11100%4/4100%1/1100%11/11
splits.js +
+
100%54/5488.46%23/26100%10/10100%51/51
workouts.js +
+
100%41/41100%22/22100%6/6100%41/41
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/splits.js.html b/backend/coverage/lcov-report/backend/routes/splits.js.html new file mode 100644 index 0000000..0b4bc6d --- /dev/null +++ b/backend/coverage/lcov-report/backend/routes/splits.js.html @@ -0,0 +1,472 @@ + + + + + + Code coverage report for backend/routes/splits.js + + + + + + + + + +
+
+

All files / backend/routes splits.js

+
+ +
+ 100% + Statements + 54/54 +
+ + +
+ 88.46% + Branches + 23/26 +
+ + +
+ 100% + Functions + 10/10 +
+ + +
+ 100% + Lines + 51/51 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +1306x +6x +6x +6x +  +6x +  +  +6x +12x +  +12x +1x +  +  +11x +1x +  +  +10x +  +  +  +  +  +10x +  +10x +  +10x +  +  +  +6x +1x +1x +  +  +  +  +6x +2x +  +  +  +  +2x +1x +  +  +1x +1x +  +1x +1x +1x +1x +  +1x +  +  +  +1x +  +  +  +6x +3x +  +  +  +  +3x +2x +  +  +1x +  +  +  +6x +4x +  +4x +1x +  +  +3x +1x +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +1x +  +  +1x +  +  +  +  +6x +2x +  +  +  +  +2x +1x +  +  +1x +  +  +6x + 
const express = require("express");
+const Split = require("../models/Split");
+const Exercise = require("../models/Exercise");
+const { protect } = require("../middleware/authMiddleware");
+ 
+const router = express.Router();
+ 
+// Create a new split
+router.post("/", protect, async (req, res) => {
+  const { name, workouts } = req.body;
+ 
+  if (!name || !name.trim()) {
+    return res.status(400).json({ message: "Split name is required" });
+  }
+ 
+  if (!Array.isArray(workouts)) {
+    return res.status(400).json({ message: "Workouts must be an array" });
+  }
+ 
+    const newSplit = new Split({
+      userId: req.user._id,
+      name: name.trim(),
+      workouts,
+    });
+ 
+    await newSplit.save();
+ 
+    const populatedSplit = await Split.findById(newSplit._id).populate("workouts");
+ 
+    res.status(201).json(populatedSplit);
+});
+ 
+// Fetch all splits
+router.get("/", protect, async (req, res) => {
+    const splits = await Split.find({ userId: req.user._id }).populate("workouts"); // Populate workouts
+    res.status(200).json(splits); // Return all splits
+ 
+});
+ 
+// Fetch a split with full exercise details per workout (for the detail modal)
+router.get("/:id/details", protect, async (req, res) => {
+    const split = await Split.findOne({
+      _id: req.params.id,
+      userId: req.user._id,
+    }).populate("workouts");
+ 
+    if (!split) {
+      return res.status(404).json({ message: "Split not found" });
+    }
+ 
+    const splitObj = split.toObject();
+    const exerciseIds = splitObj.workouts.flatMap((w) => w.exercises || []);
+ 
+    Eif (exerciseIds.length > 0) {
+      const exercises = await Exercise.find({ id: { $in: exerciseIds } });
+      const byId = new Map(exercises.map((e) => [e.id, e]));
+      splitObj.workouts = splitObj.workouts.map((w) => ({
+        ...w,
+        exercises: (w.exercises || []).map((id) => byId.get(id)).filter(Boolean),
+      }));
+    }
+ 
+    res.status(200).json(splitObj);
+});
+ 
+// Fetch a specific split by ID
+router.get("/:id", protect, async (req, res) => {
+    const split = await Split.findOne({
+      _id: req.params.id,
+      userId: req.user._id,
+    }).populate("workouts");
+ 
+    if (!split) {
+      return res.status(404).json({ message: "Split not found" }); // Handle not found
+    }
+ 
+    res.status(200).json(split); // Return the found split
+});
+ 
+// Update a specific split by ID
+router.put("/:id", protect, async (req, res) => {
+  const { name, workouts } = req.body;
+ 
+  if (!name || !name.trim()) {
+    return res.status(400).json({ message: "Split name is required" });
+  }
+ 
+  if (!Array.isArray(workouts)) {
+    return res.status(400).json({ message: "Workouts must be an array" });
+  }
+ 
+    const split = await Split.findOneAndUpdate(
+      {
+        _id: req.params.id,
+        userId: req.user._id,
+      },
+      {
+        name: name.trim(),
+        workouts,
+      },
+      {
+        new: true,
+        runValidators: true,
+      }
+    ).populate("workouts");
+ 
+    if (!split) {
+      return res.status(404).json({ message: "Split not found" });
+    }
+ 
+    res.status(200).json(split);
+ 
+});
+ 
+// Delete a specific split by ID
+router.delete("/:id", protect, async (req, res) => {
+    const split = await Split.findOneAndDelete({
+      _id: req.params.id,
+      userId: req.user._id,
+    });
+ 
+    if (!split) {
+      return res.status(404).json({ message: "Split not found" }); // Handle not found
+    }
+ 
+    res.status(200).json({ message: "Split deleted successfully" }); // Return success message
+});
+ 
+module.exports = router; // Export the router
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/workouts.js.html b/backend/coverage/lcov-report/backend/routes/workouts.js.html new file mode 100644 index 0000000..d7ecdc6 --- /dev/null +++ b/backend/coverage/lcov-report/backend/routes/workouts.js.html @@ -0,0 +1,436 @@ + + + + + + Code coverage report for backend/routes/workouts.js + + + + + + + + + +
+
+

All files / backend/routes workouts.js

+
+ +
+ 100% + Statements + 41/41 +
+ + +
+ 100% + Branches + 22/22 +
+ + +
+ 100% + Functions + 6/6 +
+ + +
+ 100% + Lines + 41/41 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+

+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +1186x +6x +6x +6x +  +6x +  +  +6x +15x +  +  +15x +1x +  +  +14x +2x +  +  +  +12x +  +  +  +  +12x +12x +  +  +  +  +6x +1x +  +  +1x +  +1x +  +  +1x +  +  +  +  +  +  +1x +  +  +  +6x +  +6x +  +  +  +  +5x +3x +  +2x +  +  +  +6x +4x +  +4x +1x +  +  +3x +1x +  +  +  +2x +  +  +  +  +  +  +  +  +  +  +  +  +  +  +2x +1x +  +  +1x +  +  +  +6x +  +2x +  +  +  +  +2x +1x +  +  +1x +  +  +  +6x + 
const express = require("express");
+const Exercise = require("../models/Exercise");
+const Workout = require("../models/Workout");
+const { protect } = require("../middleware/authMiddleware");
+ 
+const router = express.Router();
+ 
+// Create a new workout
+router.post("/", protect, async (req, res) => {
+  const { name, exercises } = req.body;
+ 
+  // Validate input
+  if (!name || !name.trim()) {
+    return res.status(400).json({ message: "Workout name is required" });
+  }
+ 
+  if (!Array.isArray(exercises) || exercises.length === 0) {
+    return res.status(400).json({ message: "Please select at least one exercise" });
+  }
+ 
+ 
+    const workout = new Workout({
+      userId: req.user._id,
+      name,
+      exercises,
+    });
+    await workout.save();
+    res.status(201).json(workout); // Return the created workout
+ 
+});
+ 
+// Fetch all workouts
+router.get("/", protect, async (req, res) => {
+    const workouts = await Workout.find({ userId: req.user._id });
+ 
+    // Populate exercises for each workout
+    const populatedWorkouts = await Promise.all(
+      workouts.map(async (workout) => {
+        const exercises = await Exercise.find({
+          id: { $in: workout.exercises },
+        });
+        return {
+          ...workout.toObject(),
+          exercises,
+        };
+      })
+    );
+ 
+    res.status(200).json(populatedWorkouts); // Return all workouts with exercises
+});
+ 
+// Fetch a specific workout by ID
+router.get("/:id", protect, async (req, res) => {
+ 
+    const workout = await Workout.findOne({
+      _id: req.params.id,
+      userId: req.user._id,
+    });
+ 
+    if (!workout) {
+      return res.status(404).json({ message: "Workout not found" });
+    }
+    res.status(200).json(workout); // Return the found workout
+});
+ 
+// Update a specific workout by ID
+router.put("/:id", protect, async (req, res) => {
+  const { name, exercises } = req.body;
+  
+  if (!name || !name.trim()) {
+    return res.status(400).json({ message: "Workout name is required" });
+  }
+ 
+  if (!Array.isArray(exercises) || exercises.length === 0) {
+    return res.status(400).json({ message: "Please select at least one exercise" });
+  }
+ 
+ 
+    const workout = await Workout.findOneAndUpdate(
+      {
+        _id: req.params.id,
+        userId: req.user._id,
+      },
+      {
+        name,
+        exercises,
+      },
+      {
+        new: true,
+        runValidators: true,
+      }
+    );
+ 
+    if (!workout) {
+      return res.status(404).json({ message: "Workout not found" });
+    }
+ 
+    res.status(200).json(workout); // Return the updated workout
+});
+ 
+// Delete a specific workout by ID
+router.delete("/:id", protect, async (req, res) => {
+ 
+    const workout = await Workout.findOneAndDelete({
+      _id: req.params.id,
+      userId: req.user._id,
+    });
+ 
+    if (!workout) {
+      return res.status(404).json({ message: "Workout not found" });
+    }
+ 
+    res.status(200).json({ message: "Workout deleted successfully" }); // Return success message
+ 
+});
+ 
+module.exports = router; // Export the router
+ 
+ +
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/base.css b/backend/coverage/lcov-report/base.css new file mode 100644 index 0000000..f418035 --- /dev/null +++ b/backend/coverage/lcov-report/base.css @@ -0,0 +1,224 @@ +body, html { + margin:0; padding: 0; + height: 100%; +} +body { + font-family: Helvetica Neue, Helvetica, Arial; + font-size: 14px; + color:#333; +} +.small { font-size: 12px; } +*, *:after, *:before { + -webkit-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing:border-box; + } +h1 { font-size: 20px; margin: 0;} +h2 { font-size: 14px; } +pre { + font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0; + padding: 0; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; +} +a { color:#0074D9; text-decoration:none; } +a:hover { text-decoration:underline; } +.strong { font-weight: bold; } +.space-top1 { padding: 10px 0 0 0; } +.pad2y { padding: 20px 0; } +.pad1y { padding: 10px 0; } +.pad2x { padding: 0 20px; } +.pad2 { padding: 20px; } +.pad1 { padding: 10px; } +.space-left2 { padding-left:55px; } +.space-right2 { padding-right:20px; } +.center { text-align:center; } +.clearfix { display:block; } +.clearfix:after { + content:''; + display:block; + height:0; + clear:both; + visibility:hidden; + } +.fl { float: left; } +@media only screen and (max-width:640px) { + .col3 { width:100%; max-width:100%; } + .hide-mobile { display:none!important; } +} + +.quiet { + color: #7f7f7f; + color: rgba(0,0,0,0.5); +} +.quiet a { opacity: 0.7; } + +.fraction { + font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font-size: 10px; + color: #555; + background: #E8E8E8; + padding: 4px 5px; + border-radius: 3px; + vertical-align: middle; +} + +div.path a:link, div.path a:visited { color: #333; } +table.coverage { + border-collapse: collapse; + margin: 10px 0 0 0; + padding: 0; +} + +table.coverage td { + margin: 0; + padding: 0; + vertical-align: top; +} +table.coverage td.line-count { + text-align: right; + padding: 0 5px 0 20px; +} +table.coverage td.line-coverage { + text-align: right; + padding-right: 10px; + min-width:20px; +} + +table.coverage td span.cline-any { + display: inline-block; + padding: 0 5px; + width: 100%; +} +.missing-if-branch { + display: inline-block; + margin-right: 5px; + border-radius: 3px; + position: relative; + padding: 0 4px; + background: #333; + color: yellow; +} + +.skip-if-branch { + display: none; + margin-right: 10px; + position: relative; + padding: 0 4px; + background: #ccc; + color: white; +} +.missing-if-branch .typ, .skip-if-branch .typ { + color: inherit !important; +} +.coverage-summary { + border-collapse: collapse; + width: 100%; +} +.coverage-summary tr { border-bottom: 1px solid #bbb; } +.keyline-all { border: 1px solid #ddd; } +.coverage-summary td, .coverage-summary th { padding: 10px; } +.coverage-summary tbody { border: 1px solid #bbb; } +.coverage-summary td { border-right: 1px solid #bbb; } +.coverage-summary td:last-child { border-right: none; } +.coverage-summary th { + text-align: left; + font-weight: normal; + white-space: nowrap; +} +.coverage-summary th.file { border-right: none !important; } +.coverage-summary th.pct { } +.coverage-summary th.pic, +.coverage-summary th.abs, +.coverage-summary td.pct, +.coverage-summary td.abs { text-align: right; } +.coverage-summary td.file { white-space: nowrap; } +.coverage-summary td.pic { min-width: 120px !important; } +.coverage-summary tfoot td { } + +.coverage-summary .sorter { + height: 10px; + width: 7px; + display: inline-block; + margin-left: 0.5em; + background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; +} +.coverage-summary .sorted .sorter { + background-position: 0 -20px; +} +.coverage-summary .sorted-desc .sorter { + background-position: 0 -10px; +} +.status-line { height: 10px; } +/* yellow */ +.cbranch-no { background: yellow !important; color: #111; } +/* dark red */ +.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } +.low .chart { border:1px solid #C21F39 } +.highlighted, +.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ + background: #C21F39 !important; +} +/* medium red */ +.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } +/* light red */ +.low, .cline-no { background:#FCE1E5 } +/* light green */ +.high, .cline-yes { background:rgb(230,245,208) } +/* medium green */ +.cstat-yes { background:rgb(161,215,106) } +/* dark green */ +.status-line.high, .high .cover-fill { background:rgb(77,146,33) } +.high .chart { border:1px solid rgb(77,146,33) } +/* dark yellow (gold) */ +.status-line.medium, .medium .cover-fill { background: #f9cd0b; } +.medium .chart { border:1px solid #f9cd0b; } +/* light yellow */ +.medium { background: #fff4c2; } + +.cstat-skip { background: #ddd; color: #111; } +.fstat-skip { background: #ddd; color: #111 !important; } +.cbranch-skip { background: #ddd !important; color: #111; } + +span.cline-neutral { background: #eaeaea; } + +.coverage-summary td.empty { + opacity: .5; + padding-top: 4px; + padding-bottom: 4px; + line-height: 1; + color: #888; +} + +.cover-fill, .cover-empty { + display:inline-block; + height: 12px; +} +.chart { + line-height: 0; +} +.cover-empty { + background: white; +} +.cover-full { + border-right: none !important; +} +pre.prettyprint { + border: none !important; + padding: 0 !important; + margin: 0 !important; +} +.com { color: #999 !important; } +.ignore-none { color: #999; font-weight: normal; } + +.wrapper { + min-height: 100%; + height: auto !important; + height: 100%; + margin: 0 auto -48px; +} +.footer, .push { + height: 48px; +} diff --git a/backend/coverage/lcov-report/block-navigation.js b/backend/coverage/lcov-report/block-navigation.js new file mode 100644 index 0000000..530d1ed --- /dev/null +++ b/backend/coverage/lcov-report/block-navigation.js @@ -0,0 +1,87 @@ +/* eslint-disable */ +var jumpToCode = (function init() { + // Classes of code we would like to highlight in the file view + var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; + + // Elements to highlight in the file listing view + var fileListingElements = ['td.pct.low']; + + // We don't want to select elements that are direct descendants of another match + var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` + + // Selector that finds elements on the page to which we can jump + var selector = + fileListingElements.join(', ') + + ', ' + + notSelector + + missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` + + // The NodeList of matching elements + var missingCoverageElements = document.querySelectorAll(selector); + + var currentIndex; + + function toggleClass(index) { + missingCoverageElements + .item(currentIndex) + .classList.remove('highlighted'); + missingCoverageElements.item(index).classList.add('highlighted'); + } + + function makeCurrent(index) { + toggleClass(index); + currentIndex = index; + missingCoverageElements.item(index).scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }); + } + + function goToPrevious() { + var nextIndex = 0; + if (typeof currentIndex !== 'number' || currentIndex === 0) { + nextIndex = missingCoverageElements.length - 1; + } else if (missingCoverageElements.length > 1) { + nextIndex = currentIndex - 1; + } + + makeCurrent(nextIndex); + } + + function goToNext() { + var nextIndex = 0; + + if ( + typeof currentIndex === 'number' && + currentIndex < missingCoverageElements.length - 1 + ) { + nextIndex = currentIndex + 1; + } + + makeCurrent(nextIndex); + } + + return function jump(event) { + if ( + document.getElementById('fileSearch') === document.activeElement && + document.activeElement != null + ) { + // if we're currently focused on the search input, we don't want to navigate + return; + } + + switch (event.which) { + case 78: // n + case 74: // j + goToNext(); + break; + case 66: // b + case 75: // k + case 80: // p + goToPrevious(); + break; + } + }; +})(); +window.addEventListener('keydown', jumpToCode); diff --git a/backend/coverage/lcov-report/favicon.png b/backend/coverage/lcov-report/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c1525b811a167671e9de1fa78aab9f5c0b61cef7 GIT binary patch literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> + + + + Code coverage report for All files + + + + + + + + + +
+
+

All files

+
+ +
+ 99.64% + Statements + 283/284 +
+ + +
+ 86.25% + Branches + 113/131 +
+ + +
+ 100% + Functions + 37/37 +
+ + +
+ 99.63% + Lines + 274/275 +
+ + +
+

+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
backend +
+
100%33/33100%6/6100%3/3100%32/32
backend/middleware +
+
100%31/31100%10/10100%3/3100%30/30
backend/models +
+
100%12/12100%0/0100%0/0100%12/12
backend/routes +
+
99.51%207/20884.34%97/115100%31/3199.5%200/201
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/backend/coverage/lcov-report/prettify.css b/backend/coverage/lcov-report/prettify.css new file mode 100644 index 0000000..b317a7c --- /dev/null +++ b/backend/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/backend/coverage/lcov-report/prettify.js b/backend/coverage/lcov-report/prettify.js new file mode 100644 index 0000000..b322523 --- /dev/null +++ b/backend/coverage/lcov-report/prettify.js @@ -0,0 +1,2 @@ +/* eslint-disable */ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/backend/coverage/lcov-report/sort-arrow-sprite.png b/backend/coverage/lcov-report/sort-arrow-sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed68316eb3f65dec9063332d2f69bf3093bbfab GIT binary patch literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc literal 0 HcmV?d00001 diff --git a/backend/coverage/lcov-report/sorter.js b/backend/coverage/lcov-report/sorter.js new file mode 100644 index 0000000..4ed70ae --- /dev/null +++ b/backend/coverage/lcov-report/sorter.js @@ -0,0 +1,210 @@ +/* eslint-disable */ +var addSorting = (function() { + 'use strict'; + var cols, + currentSort = { + index: 0, + desc: false + }; + + // returns the summary table element + function getTable() { + return document.querySelector('.coverage-summary'); + } + // returns the thead element of the summary table + function getTableHeader() { + return getTable().querySelector('thead tr'); + } + // returns the tbody element of the summary table + function getTableBody() { + return getTable().querySelector('tbody'); + } + // returns the th element for nth column + function getNthColumn(n) { + return getTableHeader().querySelectorAll('th')[n]; + } + + function onFilterInput() { + const searchValue = document.getElementById('fileSearch').value; + const rows = document.getElementsByTagName('tbody')[0].children; + + // Try to create a RegExp from the searchValue. If it fails (invalid regex), + // it will be treated as a plain text search + let searchRegex; + try { + searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive + } catch (error) { + searchRegex = null; + } + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + let isMatch = false; + + if (searchRegex) { + // If a valid regex was created, use it for matching + isMatch = searchRegex.test(row.textContent); + } else { + // Otherwise, fall back to the original plain text search + isMatch = row.textContent + .toLowerCase() + .includes(searchValue.toLowerCase()); + } + + row.style.display = isMatch ? '' : 'none'; + } + } + + // loads the search box + function addSearchBox() { + var template = document.getElementById('filterTemplate'); + var templateClone = template.content.cloneNode(true); + templateClone.getElementById('fileSearch').oninput = onFilterInput; + template.parentElement.appendChild(templateClone); + } + + // loads all columns + function loadColumns() { + var colNodes = getTableHeader().querySelectorAll('th'), + colNode, + cols = [], + col, + i; + + for (i = 0; i < colNodes.length; i += 1) { + colNode = colNodes[i]; + col = { + key: colNode.getAttribute('data-col'), + sortable: !colNode.getAttribute('data-nosort'), + type: colNode.getAttribute('data-type') || 'string' + }; + cols.push(col); + if (col.sortable) { + col.defaultDescSort = col.type === 'number'; + colNode.innerHTML = + colNode.innerHTML + ''; + } + } + return cols; + } + // attaches a data attribute to every tr element with an object + // of data values keyed by column name + function loadRowData(tableRow) { + var tableCols = tableRow.querySelectorAll('td'), + colNode, + col, + data = {}, + i, + val; + for (i = 0; i < tableCols.length; i += 1) { + colNode = tableCols[i]; + col = cols[i]; + val = colNode.getAttribute('data-value'); + if (col.type === 'number') { + val = Number(val); + } + data[col.key] = val; + } + return data; + } + // loads all row data + function loadData() { + var rows = getTableBody().querySelectorAll('tr'), + i; + + for (i = 0; i < rows.length; i += 1) { + rows[i].data = loadRowData(rows[i]); + } + } + // sorts the table using the data for the ith column + function sortByIndex(index, desc) { + var key = cols[index].key, + sorter = function(a, b) { + a = a.data[key]; + b = b.data[key]; + return a < b ? -1 : a > b ? 1 : 0; + }, + finalSorter = sorter, + tableBody = document.querySelector('.coverage-summary tbody'), + rowNodes = tableBody.querySelectorAll('tr'), + rows = [], + i; + + if (desc) { + finalSorter = function(a, b) { + return -1 * sorter(a, b); + }; + } + + for (i = 0; i < rowNodes.length; i += 1) { + rows.push(rowNodes[i]); + tableBody.removeChild(rowNodes[i]); + } + + rows.sort(finalSorter); + + for (i = 0; i < rows.length; i += 1) { + tableBody.appendChild(rows[i]); + } + } + // removes sort indicators for current column being sorted + function removeSortIndicators() { + var col = getNthColumn(currentSort.index), + cls = col.className; + + cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); + col.className = cls; + } + // adds sort indicators for current column being sorted + function addSortIndicators() { + getNthColumn(currentSort.index).className += currentSort.desc + ? ' sorted-desc' + : ' sorted'; + } + // adds event listeners for all sorter widgets + function enableUI() { + var i, + el, + ithSorter = function ithSorter(i) { + var col = cols[i]; + + return function() { + var desc = col.defaultDescSort; + + if (currentSort.index === i) { + desc = !currentSort.desc; + } + sortByIndex(i, desc); + removeSortIndicators(); + currentSort.index = i; + currentSort.desc = desc; + addSortIndicators(); + }; + }; + for (i = 0; i < cols.length; i += 1) { + if (cols[i].sortable) { + // add the click event handler on the th so users + // dont have to click on those tiny arrows + el = getNthColumn(i).querySelector('.sorter').parentElement; + if (el.addEventListener) { + el.addEventListener('click', ithSorter(i)); + } else { + el.attachEvent('onclick', ithSorter(i)); + } + } + } + } + // adds sorting functionality to the UI + return function() { + if (!getTable()) { + return; + } + cols = loadColumns(); + loadData(); + addSearchBox(); + addSortIndicators(); + enableUI(); + }; +})(); + +window.addEventListener('load', addSorting); diff --git a/backend/coverage/lcov.info b/backend/coverage/lcov.info new file mode 100644 index 0000000..a20f4cd --- /dev/null +++ b/backend/coverage/lcov.info @@ -0,0 +1,597 @@ +TN: +SF:app.js +FN:19,(anonymous_0) +FN:25,(anonymous_1) +FN:42,(anonymous_2) +FNF:3 +FNH:3 +FNDA:6,(anonymous_0) +FNDA:143,(anonymous_1) +FNDA:1,(anonymous_2) +DA:1,6 +DA:2,6 +DA:3,6 +DA:4,6 +DA:5,6 +DA:6,6 +DA:8,6 +DA:9,6 +DA:10,6 +DA:11,6 +DA:12,6 +DA:13,6 +DA:15,6 +DA:17,6 +DA:19,6 +DA:22,6 +DA:23,6 +DA:26,143 +DA:27,1 +DA:31,6 +DA:33,6 +DA:34,6 +DA:35,6 +DA:36,6 +DA:37,6 +DA:38,6 +DA:41,6 +DA:42,6 +DA:43,1 +DA:46,6 +DA:47,6 +DA:49,6 +LF:32 +LH:32 +BRDA:17,0,0,6 +BRDA:17,0,1,6 +BRDA:26,1,0,142 +BRDA:26,1,1,1 +BRDA:26,2,0,143 +BRDA:26,2,1,1 +BRF:6 +BRH:6 +end_of_record +TN: +SF:middleware/authMiddleware.js +FN:4,(anonymous_0) +FNF:1 +FNH:1 +FNDA:76,(anonymous_0) +DA:1,6 +DA:2,6 +DA:4,6 +DA:5,76 +DA:6,76 +DA:7,76 +DA:8,8 +DA:10,68 +DA:11,68 +DA:12,66 +DA:13,66 +DA:14,1 +DA:16,65 +DA:17,65 +DA:19,2 +DA:23,6 +LF:16 +LH:16 +BRDA:7,0,0,8 +BRDA:7,0,1,68 +BRDA:7,1,0,76 +BRDA:7,1,1,69 +BRDA:13,2,0,1 +BRDA:13,2,1,65 +BRF:6 +BRH:6 +end_of_record +TN: +SF:middleware/errorHandler.js +FN:2,(anonymous_0) +FNF:1 +FNH:1 +FNDA:7,(anonymous_0) +DA:2,7 +DA:3,7 +DA:4,2 +DA:6,5 +DA:7,1 +DA:9,4 +DA:10,4 +DA:13,7 +LF:8 +LH:8 +BRDA:3,0,0,2 +BRDA:3,0,1,5 +BRDA:6,1,0,1 +BRDA:6,1,1,4 +BRF:4 +BRH:4 +end_of_record +TN: +SF:middleware/rateLimiters.js +FN:4,(anonymous_0) +FNF:1 +FNH:1 +FNDA:223,(anonymous_0) +DA:1,6 +DA:4,223 +DA:6,6 +DA:15,6 +DA:24,6 +DA:33,6 +LF:6 +LH:6 +BRF:0 +BRH:0 +end_of_record +TN: +SF:models/Exercise.js +FNF:0 +FNH:0 +DA:1,6 +DA:3,6 +DA:12,6 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:models/Split.js +FNF:0 +FNH:0 +DA:1,6 +DA:3,6 +DA:25,6 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:models/User.js +FNF:0 +FNH:0 +DA:1,6 +DA:3,6 +DA:53,6 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:models/Workout.js +FNF:0 +FNH:0 +DA:1,6 +DA:3,6 +DA:25,6 +LF:3 +LH:3 +BRF:0 +BRH:0 +end_of_record +TN: +SF:routes/ai.js +FN:9,(anonymous_0) +FN:20,(anonymous_1) +FN:36,(anonymous_2) +FN:51,(anonymous_3) +FN:162,(anonymous_4) +FN:180,(anonymous_5) +FN:190,(anonymous_6) +FN:204,(anonymous_7) +FNF:8 +FNH:8 +FNDA:4,(anonymous_0) +FNDA:8,(anonymous_1) +FNDA:1,(anonymous_2) +FNDA:7,(anonymous_3) +FNDA:3,(anonymous_4) +FNDA:2,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:1,(anonymous_7) +DA:1,6 +DA:2,6 +DA:3,6 +DA:4,6 +DA:5,6 +DA:7,6 +DA:9,6 +DA:10,4 +DA:11,4 +DA:13,4 +DA:14,1 +DA:17,3 +DA:20,6 +DA:21,8 +DA:30,8 +DA:33,8 +DA:36,1 +DA:41,8 +DA:45,8 +DA:46,1 +DA:51,7 +DA:59,7 +DA:74,7 +DA:114,7 +DA:133,6 +DA:135,6 +DA:136,1 +DA:137,1 +DA:138,1 +DA:139,1 +DA:140,1 +DA:142,1 +DA:152,5 +DA:154,5 +DA:155,1 +DA:160,4 +DA:162,3 +DA:164,3 +DA:165,3 +DA:166,3 +DA:167,1 +DA:175,2 +DA:180,6 +DA:181,2 +DA:183,2 +DA:184,1 +DA:187,1 +DA:189,1 +DA:190,1 +DA:192,1 +DA:198,1 +DA:201,1 +DA:204,1 +DA:207,1 +DA:214,6 +LF:55 +LH:55 +BRDA:13,0,0,1 +BRDA:13,0,1,3 +BRDA:13,1,0,4 +BRDA:13,1,1,3 +BRDA:21,2,0,8 +BRDA:21,2,1,0 +BRDA:24,3,0,0 +BRDA:25,4,0,0 +BRDA:26,5,0,0 +BRDA:27,6,0,0 +BRDA:28,7,0,0 +BRDA:29,8,0,0 +BRDA:33,9,0,1 +BRDA:33,9,1,7 +BRDA:45,10,0,1 +BRDA:45,10,1,7 +BRDA:82,11,0,7 +BRDA:82,11,1,6 +BRDA:83,12,0,7 +BRDA:83,12,1,7 +BRDA:135,13,0,1 +BRDA:135,13,1,5 +BRDA:142,14,0,1 +BRDA:142,14,1,0 +BRDA:144,15,0,1 +BRDA:144,15,1,0 +BRDA:154,16,0,1 +BRDA:154,16,1,4 +BRDA:164,17,0,3 +BRDA:164,17,1,0 +BRDA:165,18,0,3 +BRDA:165,18,1,0 +BRDA:166,19,0,1 +BRDA:166,19,1,2 +BRDA:183,20,0,1 +BRDA:183,20,1,1 +BRDA:183,21,0,2 +BRDA:183,21,1,2 +BRDA:190,22,0,1 +BRDA:190,22,1,0 +BRDA:194,23,0,1 +BRDA:194,23,1,0 +BRDA:203,24,0,1 +BRDA:203,24,1,0 +BRF:44 +BRH:30 +end_of_record +TN: +SF:routes/auth.js +FN:9,(anonymous_0) +FN:15,(anonymous_1) +FN:22,(anonymous_2) +FN:54,(anonymous_3) +FN:79,(anonymous_4) +FN:85,(anonymous_5) +FNF:6 +FNH:6 +FNDA:56,(anonymous_0) +FNDA:59,(anonymous_1) +FNDA:58,(anonymous_2) +FNDA:4,(anonymous_3) +FNDA:1,(anonymous_4) +FNDA:2,(anonymous_5) +DA:1,6 +DA:2,6 +DA:3,6 +DA:4,6 +DA:5,6 +DA:7,6 +DA:9,6 +DA:10,56 +DA:15,59 +DA:22,6 +DA:23,58 +DA:25,58 +DA:26,1 +DA:29,57 +DA:30,1 +DA:33,56 +DA:35,56 +DA:36,1 +DA:39,55 +DA:41,55 +DA:47,55 +DA:54,6 +DA:55,4 +DA:57,4 +DA:58,1 +DA:61,3 +DA:63,3 +DA:64,1 +DA:67,2 +DA:69,2 +DA:70,1 +DA:73,1 +DA:79,6 +DA:80,1 +DA:85,6 +DA:94,2 +DA:96,2 +DA:98,2 +DA:99,0 +DA:102,2 +DA:112,2 +DA:114,2 +DA:118,6 +LF:43 +LH:42 +BRDA:25,0,0,1 +BRDA:25,0,1,57 +BRDA:25,1,0,58 +BRDA:25,1,1,57 +BRDA:25,1,2,57 +BRDA:29,2,0,1 +BRDA:29,2,1,56 +BRDA:35,3,0,1 +BRDA:35,3,1,55 +BRDA:57,4,0,1 +BRDA:57,4,1,3 +BRDA:57,5,0,4 +BRDA:57,5,1,4 +BRDA:63,6,0,1 +BRDA:63,6,1,2 +BRDA:69,7,0,1 +BRDA:69,7,1,1 +BRDA:98,8,0,0 +BRDA:98,8,1,2 +BRF:19 +BRH:18 +end_of_record +TN: +SF:routes/exercises.js +FN:7,(anonymous_0) +FNF:1 +FNH:1 +FNDA:3,(anonymous_0) +DA:1,6 +DA:2,6 +DA:4,6 +DA:7,6 +DA:8,3 +DA:10,3 +DA:11,3 +DA:12,1 +DA:22,3 +DA:30,3 +DA:33,6 +LF:11 +LH:11 +BRDA:8,0,0,2 +BRDA:8,1,0,2 +BRDA:11,2,0,1 +BRDA:11,2,1,2 +BRF:4 +BRH:4 +end_of_record +TN: +SF:routes/splits.js +FN:9,(anonymous_0) +FN:34,(anonymous_1) +FN:41,(anonymous_2) +FN:52,(anonymous_3) +FN:56,(anonymous_4) +FN:57,(anonymous_5) +FN:59,(anonymous_6) +FN:67,(anonymous_7) +FN:81,(anonymous_8) +FN:116,(anonymous_9) +FNF:10 +FNH:10 +FNDA:12,(anonymous_0) +FNDA:1,(anonymous_1) +FNDA:2,(anonymous_2) +FNDA:1,(anonymous_3) +FNDA:1,(anonymous_4) +FNDA:1,(anonymous_5) +FNDA:1,(anonymous_6) +FNDA:3,(anonymous_7) +FNDA:4,(anonymous_8) +FNDA:2,(anonymous_9) +DA:1,6 +DA:2,6 +DA:3,6 +DA:4,6 +DA:6,6 +DA:9,6 +DA:10,12 +DA:12,12 +DA:13,1 +DA:16,11 +DA:17,1 +DA:20,10 +DA:26,10 +DA:28,10 +DA:30,10 +DA:34,6 +DA:35,1 +DA:36,1 +DA:41,6 +DA:42,2 +DA:47,2 +DA:48,1 +DA:51,1 +DA:52,1 +DA:54,1 +DA:55,1 +DA:56,1 +DA:57,1 +DA:59,1 +DA:63,1 +DA:67,6 +DA:68,3 +DA:73,3 +DA:74,2 +DA:77,1 +DA:81,6 +DA:82,4 +DA:84,4 +DA:85,1 +DA:88,3 +DA:89,1 +DA:92,2 +DA:107,2 +DA:108,1 +DA:111,1 +DA:116,6 +DA:117,2 +DA:122,2 +DA:123,1 +DA:126,1 +DA:129,6 +LF:51 +LH:51 +BRDA:12,0,0,1 +BRDA:12,0,1,11 +BRDA:12,1,0,12 +BRDA:12,1,1,11 +BRDA:16,2,0,1 +BRDA:16,2,1,10 +BRDA:47,3,0,1 +BRDA:47,3,1,1 +BRDA:52,4,0,1 +BRDA:52,4,1,0 +BRDA:54,5,0,1 +BRDA:54,5,1,0 +BRDA:59,6,0,1 +BRDA:59,6,1,0 +BRDA:73,7,0,2 +BRDA:73,7,1,1 +BRDA:84,8,0,1 +BRDA:84,8,1,3 +BRDA:84,9,0,4 +BRDA:84,9,1,3 +BRDA:88,10,0,1 +BRDA:88,10,1,2 +BRDA:107,11,0,1 +BRDA:107,11,1,1 +BRDA:122,12,0,1 +BRDA:122,12,1,1 +BRF:26 +BRH:23 +end_of_record +TN: +SF:routes/workouts.js +FN:9,(anonymous_0) +FN:33,(anonymous_1) +FN:38,(anonymous_2) +FN:53,(anonymous_3) +FN:67,(anonymous_4) +FN:102,(anonymous_5) +FNF:6 +FNH:6 +FNDA:15,(anonymous_0) +FNDA:1,(anonymous_1) +FNDA:1,(anonymous_2) +FNDA:6,(anonymous_3) +FNDA:4,(anonymous_4) +FNDA:2,(anonymous_5) +DA:1,6 +DA:2,6 +DA:3,6 +DA:4,6 +DA:6,6 +DA:9,6 +DA:10,15 +DA:13,15 +DA:14,1 +DA:17,14 +DA:18,2 +DA:22,12 +DA:27,12 +DA:28,12 +DA:33,6 +DA:34,1 +DA:37,1 +DA:39,1 +DA:42,1 +DA:49,1 +DA:53,6 +DA:55,6 +DA:60,5 +DA:61,3 +DA:63,2 +DA:67,6 +DA:68,4 +DA:70,4 +DA:71,1 +DA:74,3 +DA:75,1 +DA:79,2 +DA:94,2 +DA:95,1 +DA:98,1 +DA:102,6 +DA:104,2 +DA:109,2 +DA:110,1 +DA:113,1 +DA:117,6 +LF:41 +LH:41 +BRDA:13,0,0,1 +BRDA:13,0,1,14 +BRDA:13,1,0,15 +BRDA:13,1,1,14 +BRDA:17,2,0,2 +BRDA:17,2,1,12 +BRDA:17,3,0,14 +BRDA:17,3,1,13 +BRDA:60,4,0,3 +BRDA:60,4,1,2 +BRDA:70,5,0,1 +BRDA:70,5,1,3 +BRDA:70,6,0,4 +BRDA:70,6,1,3 +BRDA:74,7,0,1 +BRDA:74,7,1,2 +BRDA:74,8,0,3 +BRDA:74,8,1,3 +BRDA:94,9,0,1 +BRDA:94,9,1,1 +BRDA:109,10,0,1 +BRDA:109,10,1,1 +BRF:22 +BRH:22 +end_of_record diff --git a/backend/middleware/authMiddleware.js b/backend/middleware/authMiddleware.js index 2303ac0..1694131 100644 --- a/backend/middleware/authMiddleware.js +++ b/backend/middleware/authMiddleware.js @@ -2,36 +2,22 @@ const jwt = require("jsonwebtoken"); const User = require("../models/User"); const protect = async (req, res, next) => { - try { - const authHeader = req.headers.authorization; - - if (!authHeader || !authHeader.startsWith("Bearer ")) { - return res.status(401).json({ message: "Not authorized, no token" }); - } - - const token = authHeader.split(" ")[1]; - - const decoded = jwt.verify(token, process.env.JWT_SECRET); - - const user = await User.findById(decoded.userId).select("-passwordHash"); - - if (!user) { - return res.status(401).json({ message: "Not authorized, user not found" }); - } - - req.user = user; - next(); - - } catch (error) { - return res.status(401).json({ message: "Not authorized, token failed" }); + try { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ message: "Not authorized, no token" }); } -}; - -const adminOnly = (req, res, next) => { - if (!req.user || !req.user.isAdmin) { - return res.status(403).json({ message: "Admin access required" }); + const token = authHeader.split(" ")[1]; + const decoded = jwt.verify(token, process.env.JWT_SECRET); + const user = await User.findById(decoded.userId).select("-passwordHash"); + if (!user) { + return res.status(401).json({ message: "Not authorized, user not found" }); } + req.user = user; next(); + } catch (error) { + return res.status(401).json({ message: "Not authorized, token failed" }); + } }; -module.exports = { protect, adminOnly }; +module.exports = { protect }; \ No newline at end of file diff --git a/backend/middleware/errorHandler.js b/backend/middleware/errorHandler.js new file mode 100644 index 0000000..f1a914c --- /dev/null +++ b/backend/middleware/errorHandler.js @@ -0,0 +1,13 @@ +// Central error handler — must be the LAST app.use(). +const errorHandler = (err, req, res, next) => { + if (err.name === "CastError") { + return res.status(400).json({ message: "Invalid ID format" }); // bad ObjectId + } + if (err.name === "ValidationError") { + return res.status(400).json({ message: err.message }); // mongoose validation + } + console.error("Unhandled error:", err.message); + res.status(500).json({ message: "Server Error" }); +}; + +module.exports = errorHandler; \ No newline at end of file diff --git a/backend/middleware/rateLimiters.js b/backend/middleware/rateLimiters.js index 65768f8..f6dace6 100644 --- a/backend/middleware/rateLimiters.js +++ b/backend/middleware/rateLimiters.js @@ -1,32 +1,32 @@ const rateLimit = require("express-rate-limit"); +// Disable rate limiting during tests so the suite isn't throttled. const skip = () => process.env.NODE_ENV === "test"; -// Applied to every /api route as a baseline guard against abuse. const apiLimiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 300, - standardHeaders: true, - legacyHeaders: false, - skip, - message: { message: "Too many requests, please try again later." }, + windowMs: 15 * 60 * 1000, + max: 300, + standardHeaders: true, + legacyHeaders: false, + skip, + message: { message: "Too many requests, please try again later." }, }); -// Tighter limit for auth endpoints to slow down credential stuffing / brute force. const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20, standardHeaders: true, legacyHeaders: false, + skip, message: { message: "Too many authentication attempts, please try again later." }, }); -// AI generation is expensive (per-call LLM cost), so it gets the strictest limit. const aiLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour + windowMs: 60 * 60 * 1000, max: 30, standardHeaders: true, legacyHeaders: false, + skip, message: { message: "AI generation limit reached, please try again later." }, }); diff --git a/backend/models/User.js b/backend/models/User.js index 1333089..1679fcb 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -18,10 +18,6 @@ const userSchema = new mongoose.Schema( type: String, required: true, }, - isAdmin: { - type: Boolean, - default: false, - }, fitnessProfile: { goal: { type: String, diff --git a/backend/package-lock.json b/backend/package-lock.json index bf6b907..3093c6f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -15,6 +15,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", + "express-async-errors": "^3.1.1", "express-rate-limit": "^8.5.2", "helmet": "^8.2.0", "jsonwebtoken": "^9.0.3", @@ -2974,6 +2975,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "license": "ISC", + "peerDependencies": { + "express": "^4.16.2" + } + }, "node_modules/express-rate-limit": { "version": "8.5.2", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", diff --git a/backend/package.json b/backend/package.json index 5be585f..9db130a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,11 +5,16 @@ "scripts": { "start": "node server.js", "dev": "nodemon server.js", - "test": "jest --runInBand --forceExit" + "test": "jest --runInBand --forceExit", + "seed": "node scripts/seedExercises.js", + "test:coverage": "jest --runInBand --forceExit --coverage" }, "jest": { "testEnvironment": "node", - "setupFilesAfterEnv": ["/tests/setup.js"] + "setupFilesAfterEnv": ["/tests/setup.js"], + "coverageThreshold": { + "global": { "statements": 90, "branches": 80, "functions": 90, "lines": 90 } + } }, "keywords": [], "author": "", @@ -21,6 +26,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.0", + "express-async-errors": "^3.1.1", "express-rate-limit": "^8.5.2", "helmet": "^8.2.0", "jsonwebtoken": "^9.0.3", diff --git a/backend/routes/ai.js b/backend/routes/ai.js index 8e43f60..5523a6b 100644 --- a/backend/routes/ai.js +++ b/backend/routes/ai.js @@ -18,46 +18,45 @@ const extractJson = (text) => { }; router.post("/generate-workout", protect, async (req, res) => { - try { - const profile = req.user.fitnessProfile || {}; - - const { - goal = "general_fitness", - experienceLevel = "beginner", - daysPerWeek = 3, - sessionLengthMinutes = 60, - equipment = [], - limitations = [], - } = profile; - - const equipmentFilter = - equipment.length > 0 - ? { - equipment: { - $in: equipment.map((item) => new RegExp(item, "i")), - }, - } - : {}; - - const exercises = await Exercise.find(equipmentFilter) - .limit(40) - .select("id name bodyPart equipment target"); - - if (exercises.length === 0) { - return res.status(400).json({ - message: "No matching exercises found for this profile.", - }); - } + const profile = req.user.fitnessProfile || {}; + + const { + goal = "general_fitness", + experienceLevel = "beginner", + daysPerWeek = 3, + sessionLengthMinutes = 60, + equipment = [], + limitations = [], + } = profile; + + const equipmentFilter = + equipment.length > 0 + ? { + equipment: { + $in: equipment.map((item) => new RegExp(item, "i")), + }, + } + : {}; - const exerciseOptions = exercises.map((exercise) => ({ - id: exercise.id, - name: exercise.name, - bodyPart: exercise.bodyPart, - equipment: exercise.equipment, - target: exercise.target, - })); + const exercises = await Exercise.find(equipmentFilter) + .limit(40) + .select("id name bodyPart equipment target"); - const systemPrompt = ` + if (exercises.length === 0) { + return res.status(400).json({ + message: "No matching exercises found for this profile.", + }); + } + + const exerciseOptions = exercises.map((exercise) => ({ + id: exercise.id, + name: exercise.name, + bodyPart: exercise.bodyPart, + equipment: exercise.equipment, + target: exercise.target, + })); + + const systemPrompt = ` You are FitForge AI, a workout planning assistant. Return ONLY valid JSON. @@ -72,7 +71,7 @@ Do not provide medical advice. If limitations are listed, avoid exercises that obviously conflict with them. `; - const userPrompt = ` + const userPrompt = ` Create a personalized workout plan. User profile: @@ -112,118 +111,104 @@ Return JSON in this exact shape: } `; - const llmResponse = await fetch(process.env.LLM_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.LLM_API_KEY}`, + const llmResponse = await fetch(process.env.LLM_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${process.env.LLM_API_KEY}`, + }, + body: JSON.stringify({ + model: process.env.LLM_MODEL, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: userPrompt }, + ], + temperature: 0.2, + response_format: { + type: "json_object", }, - body: JSON.stringify({ - model: process.env.LLM_MODEL, - messages: [ - { role: "system", content: systemPrompt }, - { role: "user", content: userPrompt }, - ], - temperature: 0.2, - response_format: { - type: "json_object", - }, - }), + }), + }); + + const llmData = await llmResponse.json(); + + if (!llmResponse.ok) { + console.error("LLM status:", llmResponse.status); + console.error("Retry after:", llmResponse.headers.get("retry-after")); + console.error("Rate limit remaining requests:", llmResponse.headers.get("x-ratelimit-remaining-requests")); + console.error("Rate limit remaining tokens:", llmResponse.headers.get("x-ratelimit-remaining-tokens")); + console.error("LLM request failed:", JSON.stringify(llmData, null, 2)); + + return res.status(llmResponse.status === 429 ? 429 : 500).json({ + message: + llmResponse.status === 429 + ? "Rate limit hit. Please wait and try again." + : "LLM request failed", + retryAfter: llmResponse.headers.get("retry-after"), + error: llmData, }); + } - const llmData = await llmResponse.json(); - - if (!llmResponse.ok) { - console.error("LLM status:", llmResponse.status); - console.error("Retry after:", llmResponse.headers.get("retry-after")); - console.error("Rate limit remaining requests:", llmResponse.headers.get("x-ratelimit-remaining-requests")); - console.error("Rate limit remaining tokens:", llmResponse.headers.get("x-ratelimit-remaining-tokens")); - console.error("LLM request failed:", JSON.stringify(llmData, null, 2)); - - return res.status(llmResponse.status === 429 ? 429 : 500).json({ - message: llmResponse.status === 429 - ? "Rate limit hit. Please wait and try again." - : "LLM request failed", - retryAfter: llmResponse.headers.get("retry-after"), - error: llmData, - }); - } - - const content = llmData.choices?.[0]?.message?.content; + const content = llmData.choices?.[0]?.message?.content; - if (!content) { - return res.status(500).json({ - message: "LLM returned no content", - }); - } + if (!content) { + return res.status(500).json({ + message: "LLM returned no content", + }); + } - const plan = extractJson(content); + const plan = extractJson(content); - const validExerciseIds = new Set(exerciseOptions.map((exercise) => exercise.id)); + const validExerciseIds = new Set(exerciseOptions.map((exercise) => exercise.id)); - for (const day of plan.days || []) { - for (const exercise of day.exercises || []) { - if (!validExerciseIds.has(exercise.exerciseId)) { - return res.status(500).json({ - message: "LLM returned an invalid exercise ID", - invalidExerciseId: exercise.exerciseId, - }); - } + for (const day of plan.days || []) { + for (const exercise of day.exercises || []) { + if (!validExerciseIds.has(exercise.exerciseId)) { + return res.status(500).json({ + message: "LLM returned an invalid exercise ID", + invalidExerciseId: exercise.exerciseId, + }); } } - - res.status(200).json({ - plan, - }); - } catch (error) { - console.error("Error generating workout plan:", error); - res.status(500).json({ - message: "Failed to generate workout plan", - error: error.message, - }); } + + res.status(200).json({ + plan, + }); }); router.post("/save-generated-plan", protect, async (req, res) => { - try { - const { plan } = req.body; - - if (!plan || !Array.isArray(plan.days)) { - return res.status(400).json({ message: "Valid plan is required" }); - } + const { plan } = req.body; - const createdWorkouts = []; - - for (const day of plan.days) { - const exerciseIds = (day.exercises || []).map((exercise) => exercise.exerciseId); + if (!plan || !Array.isArray(plan.days)) { + return res.status(400).json({ message: "Valid plan is required" }); + } - const workout = await Workout.create({ - userId: req.user._id, - name: day.name || `Day ${day.day}`, - exercises: exerciseIds, - }); + const createdWorkouts = []; - createdWorkouts.push(workout); - } + for (const day of plan.days) { + const exerciseIds = (day.exercises || []).map((exercise) => exercise.exerciseId); - const split = await Split.create({ + const workout = await Workout.create({ userId: req.user._id, - name: plan.programName || "AI Generated Plan", - workouts: createdWorkouts.map((workout) => workout._id), + name: day.name || `Day ${day.day}`, + exercises: exerciseIds, }); - res.status(201).json({ - message: "AI plan saved successfully", - split, - workouts: createdWorkouts, - }); - } catch (error) { - console.error("Error saving generated plan:", error); - res.status(500).json({ - message: "Failed to save generated plan", - error: error.message, - }); + createdWorkouts.push(workout); } + + const split = await Split.create({ + userId: req.user._id, + name: plan.programName || "AI Generated Plan", + workouts: createdWorkouts.map((workout) => workout._id), + }); + + res.status(201).json({ + message: "AI plan saved successfully", + split, + workouts: createdWorkouts, + }); }); module.exports = router; \ No newline at end of file diff --git a/backend/routes/auth.js b/backend/routes/auth.js index f9d346f..e9324d5 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -20,7 +20,6 @@ const serializeUser = (user) => ({ }); router.post("/register", async (req, res) => { - try { const { name, email, password } = req.body; if (!name || !email || !password) { @@ -50,13 +49,9 @@ router.post("/register", async (req, res) => { user: serializeUser(user), }); - } catch (error) { - res.status(500).json({ message: "Registration failed", error: error.message }); - } }); router.post("/login", async (req, res) => { - try { const { email, password } = req.body; if (!email || !password) { @@ -79,9 +74,6 @@ router.post("/login", async (req, res) => { token: createToken(user._id), user: serializeUser(user), }); - } catch (error) { - res.status(500).json({ message: "Login Failed", error: error.message }); - } }); router.get("/me", protect, async (req, res) => { @@ -91,7 +83,7 @@ router.get("/me", protect, async (req, res) => { }); router.put("/profile", protect, async (req, res) => { - try { + const { goal, experienceLevel, @@ -121,12 +113,6 @@ router.put("/profile", protect, async (req, res) => { res.status(200).json({ user: serializeUser(user) }); - } catch (error) { - res.status(500).json({ - message: "Failed to update fitness profile", - error: error.message, - }) - } }); module.exports = router; \ No newline at end of file diff --git a/backend/routes/exercises.js b/backend/routes/exercises.js index 5790698..397f8bc 100644 --- a/backend/routes/exercises.js +++ b/backend/routes/exercises.js @@ -1,47 +1,13 @@ const express = require("express"); -const axios = require("axios"); const Exercise = require("../models/Exercise"); -const { protect, adminOnly } = require("../middleware/authMiddleware"); const router = express.Router(); -// Fetch exercises from the external API -router.get("/fetch", protect, adminOnly, async (req, res) => { - try { - const response = await axios.get( - "https://exercisedb.p.rapidapi.com/exercises", - { - headers: { - "x-rapidapi-key": process.env.EXERCISE_DB_API_KEY, - "x-rapidapi-host": "exercisedb.p.rapidapi.com", - }, - params: { - limit: 9999, - }, - } - ); - - const exercises = response.data; - - await Exercise.deleteMany({}); // Clear existing exercises - - await Exercise.insertMany(exercises); // Insert fetched exercises - - res - .status(200) - .json({ message: `${exercises.length} exercises fetched and stored` }); - } catch (error) { - console.error("Error fetching exercises:", error); - res.status(500).json({ message: "Server Error", error: error.message }); - } -}); - // Get exercises with optional search and pagination router.get("/", async (req, res) => { const { search, page = 1, limit = 100 } = req.query; let query = {}; - if (search) { query = { $or: [ @@ -53,7 +19,6 @@ router.get("/", async (req, res) => { }; } - try { const exercises = await Exercise.aggregate([ { $match: query }, { $group: { _id: "$name", doc: { $first: "$$ROOT" } } }, @@ -62,12 +27,7 @@ router.get("/", async (req, res) => { { $skip: (page - 1) * limit }, { $limit: parseInt(limit) }, ]); - res.status(200).json(exercises); - } catch (error) { - console.error("Error fetching exercises:", error); - res.status(500).json({ message: "Server Error", error }); - } }); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/backend/routes/splits.js b/backend/routes/splits.js index 48d7b75..3698840 100644 --- a/backend/routes/splits.js +++ b/backend/routes/splits.js @@ -1,5 +1,6 @@ const express = require("express"); const Split = require("../models/Split"); +const Exercise = require("../models/Exercise"); const { protect } = require("../middleware/authMiddleware"); const router = express.Router(); @@ -16,7 +17,6 @@ router.post("/", protect, async (req, res) => { return res.status(400).json({ message: "Workouts must be an array" }); } - try { const newSplit = new Split({ userId: req.user._id, name: name.trim(), @@ -28,26 +28,43 @@ router.post("/", protect, async (req, res) => { const populatedSplit = await Split.findById(newSplit._id).populate("workouts"); res.status(201).json(populatedSplit); - } catch (error) { - console.error("Error creating split:", error); - res.status(500).json({ message: "Failed to create split", error: error.message }); - } }); // Fetch all splits router.get("/", protect, async (req, res) => { - try { const splits = await Split.find({ userId: req.user._id }).populate("workouts"); // Populate workouts res.status(200).json(splits); // Return all splits - } catch (error) { - console.error("Error fetching splits:", error); - res.status(500).json({ message: "Server Error", error }); // Handle errors - } + +}); + +// Fetch a split with full exercise details per workout (for the detail modal) +router.get("/:id/details", protect, async (req, res) => { + const split = await Split.findOne({ + _id: req.params.id, + userId: req.user._id, + }).populate("workouts"); + + if (!split) { + return res.status(404).json({ message: "Split not found" }); + } + + const splitObj = split.toObject(); + const exerciseIds = splitObj.workouts.flatMap((w) => w.exercises || []); + + if (exerciseIds.length > 0) { + const exercises = await Exercise.find({ id: { $in: exerciseIds } }); + const byId = new Map(exercises.map((e) => [e.id, e])); + splitObj.workouts = splitObj.workouts.map((w) => ({ + ...w, + exercises: (w.exercises || []).map((id) => byId.get(id)).filter(Boolean), + })); + } + + res.status(200).json(splitObj); }); // Fetch a specific split by ID router.get("/:id", protect, async (req, res) => { - try { const split = await Split.findOne({ _id: req.params.id, userId: req.user._id, @@ -58,10 +75,6 @@ router.get("/:id", protect, async (req, res) => { } res.status(200).json(split); // Return the found split - } catch (error) { - console.error("Error fetching split:", error); - res.status(500).json({ message: "Server Error", error }); // Handle errors - } }); // Update a specific split by ID @@ -76,7 +89,6 @@ router.put("/:id", protect, async (req, res) => { return res.status(400).json({ message: "Workouts must be an array" }); } - try { const split = await Split.findOneAndUpdate( { _id: req.params.id, @@ -97,15 +109,11 @@ router.put("/:id", protect, async (req, res) => { } res.status(200).json(split); - } catch (error) { - console.error("Error updating split:", error); - res.status(500).json({ message: "Failed to update split", error: error.message }); - } + }); // Delete a specific split by ID router.delete("/:id", protect, async (req, res) => { - try { const split = await Split.findOneAndDelete({ _id: req.params.id, userId: req.user._id, @@ -116,10 +124,6 @@ router.delete("/:id", protect, async (req, res) => { } res.status(200).json({ message: "Split deleted successfully" }); // Return success message - } catch (error) { - console.error("Error deleting split:", error); - res.status(500).json({ message: "Server Error", error }); // Handle errors - } }); module.exports = router; // Export the router diff --git a/backend/routes/workouts.js b/backend/routes/workouts.js index 9eda3ed..f153fe7 100644 --- a/backend/routes/workouts.js +++ b/backend/routes/workouts.js @@ -18,7 +18,7 @@ router.post("/", protect, async (req, res) => { return res.status(400).json({ message: "Please select at least one exercise" }); } - try { + const workout = new Workout({ userId: req.user._id, name, @@ -26,15 +26,11 @@ router.post("/", protect, async (req, res) => { }); await workout.save(); res.status(201).json(workout); // Return the created workout - } catch (error) { - console.error("Error creating workout:", error); - res.status(500).json({ message: "Server Error", error: error.message }); - } + }); // Fetch all workouts router.get("/", protect, async (req, res) => { - try { const workouts = await Workout.find({ userId: req.user._id }); // Populate exercises for each workout @@ -51,15 +47,11 @@ router.get("/", protect, async (req, res) => { ); res.status(200).json(populatedWorkouts); // Return all workouts with exercises - } catch (error) { - console.error("Error fetching workouts:", error.message); - res.status(500).json({ message: "Server Error", error: error.message }); - } }); // Fetch a specific workout by ID router.get("/:id", protect, async (req, res) => { - try { + const workout = await Workout.findOne({ _id: req.params.id, userId: req.user._id, @@ -69,10 +61,6 @@ router.get("/:id", protect, async (req, res) => { return res.status(404).json({ message: "Workout not found" }); } res.status(200).json(workout); // Return the found workout - } catch (error) { - console.error("Error fetching workout:", error); - res.status(500).json({ message: "Server Error", error }); - } }); // Update a specific workout by ID @@ -87,7 +75,7 @@ router.put("/:id", protect, async (req, res) => { return res.status(400).json({ message: "Please select at least one exercise" }); } - try { + const workout = await Workout.findOneAndUpdate( { _id: req.params.id, @@ -108,15 +96,11 @@ router.put("/:id", protect, async (req, res) => { } res.status(200).json(workout); // Return the updated workout - } catch (error) { - console.error("Error updating workout:", error); - res.status(500).json({ message: "Server Error", error }); - } }); // Delete a specific workout by ID router.delete("/:id", protect, async (req, res) => { - try { + const workout = await Workout.findOneAndDelete({ _id: req.params.id, userId: req.user._id, @@ -127,10 +111,7 @@ router.delete("/:id", protect, async (req, res) => { } res.status(200).json({ message: "Workout deleted successfully" }); // Return success message - } catch (error) { - console.error("Error deleting workout:", error); - res.status(500).json({ message: "Server Error", error }); - } + }); module.exports = router; // Export the router diff --git a/backend/scripts/seedExercises.js b/backend/scripts/seedExercises.js new file mode 100644 index 0000000..c937984 --- /dev/null +++ b/backend/scripts/seedExercises.js @@ -0,0 +1,22 @@ +const request = require("supertest"); +const app = require("../app"); +const { protect, adminOnly } = require("../middleware/authMiddleware"); + +describe("Security middleware", () => { + it("exports protect and adminOnly as functions", () => { + expect(typeof protect).toBe("function"); + expect(typeof adminOnly).toBe("function"); + }); + + it("rejects a protected route with no token (401)", async () => { + const res = await request(app).get("/api/workouts"); + expect(res.status).toBe(401); + }); + + it("rejects a protected route with an invalid token (401)", async () => { + const res = await request(app) + .get("/api/workouts") + .set("Authorization", "Bearer not-a-real-token"); + expect(res.status).toBe(401); + }); +}); \ No newline at end of file diff --git a/backend/tests/ai.test.js b/backend/tests/ai.test.js new file mode 100644 index 0000000..6af4149 --- /dev/null +++ b/backend/tests/ai.test.js @@ -0,0 +1,113 @@ +const request = require("supertest"); +const app = require("../app"); +const Exercise = require("../models/Exercise"); + +const auth = (token) => ({ Authorization: `Bearer ${token}` }); +async function registerUser(email) { + const res = await request(app).post("/api/auth/register").send({ name: "Test", email, password: "password123" }); + return res.body.token; +} + +// Fake LLM HTTP response shaped like what the route expects +function mockLLM({ ok = true, status = 200, content = "", body = null } = {}) { + return { + ok, status, + headers: { get: () => null }, + json: async () => (body !== null ? body : { choices: [{ message: { content } }] }), + }; +} + +const validPlan = { + programName: "Test Program", goal: "strength", daysPerWeek: 1, + days: [{ day: 1, name: "Day 1", focus: "full body", + exercises: [{ exerciseId: 1, name: "Bench Press", sets: 3, repRange: "8-10", restSeconds: 90, reason: "x" }] }], + notes: [], +}; + +describe("AI API", () => { + let token; + beforeEach(async () => { + token = await registerUser("ai-user@example.com"); + global.fetch = jest.fn(); // mock the LLM call + process.env.LLM_API_URL = "https://mock-llm.test/chat"; + process.env.LLM_API_KEY = "test-key"; + process.env.LLM_MODEL = "test-model"; + }); + afterEach(() => jest.restoreAllMocks()); + + describe("POST /api/ai/generate-workout", () => { + it("returns a plan when exercises exist and the LLM responds (200)", async () => { + await Exercise.create({ id: 1, name: "Bench Press", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }); + global.fetch.mockResolvedValue(mockLLM({ content: JSON.stringify(validPlan) })); + const res = await request(app).post("/api/ai/generate-workout").set(auth(token)); + expect(res.status).toBe(200); + expect(res.body.plan.programName).toBe("Test Program"); + }); + it("returns 400 when there are no exercises to choose from", async () => { + global.fetch.mockResolvedValue(mockLLM({ content: JSON.stringify(validPlan) })); + const res = await request(app).post("/api/ai/generate-workout").set(auth(token)); + expect(res.status).toBe(400); + expect(global.fetch).not.toHaveBeenCalled(); // bails before calling the LLM + }); + it("rejects a plan referencing an invalid exercise id (500)", async () => { + await Exercise.create({ id: 1, name: "Bench Press", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }); + const badPlan = { ...validPlan, days: [{ ...validPlan.days[0], + exercises: [{ exerciseId: 999, name: "Fake", sets: 1, repRange: "1", restSeconds: 1, reason: "x" }] }] }; + global.fetch.mockResolvedValue(mockLLM({ content: JSON.stringify(badPlan) })); + const res = await request(app).post("/api/ai/generate-workout").set(auth(token)); + expect(res.status).toBe(500); + }); + it("propagates a 429 from the LLM", async () => { + await Exercise.create({ id: 1, name: "Bench Press", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }); + global.fetch.mockResolvedValue(mockLLM({ ok: false, status: 429, body: { error: "rate limited" } })); + const res = await request(app).post("/api/ai/generate-workout").set(auth(token)); + expect(res.status).toBe(429); + }); + it("requires authentication (401)", async () => { + const res = await request(app).post("/api/ai/generate-workout"); + expect(res.status).toBe(401); + }); + it("uses the equipment filter when the profile has equipment", async () => { + await Exercise.create({ id: 1, name: "Bench", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }); + await request(app).put("/api/auth/profile").set(auth(token)).send({ equipment: ["barbell"] }); + global.fetch.mockResolvedValue(mockLLM({ content: JSON.stringify(validPlan) })); + const res = await request(app).post("/api/ai/generate-workout").set(auth(token)); + expect(res.status).toBe(200); + }); + }); + + describe("POST /api/ai/save-generated-plan", () => { + it("saves the plan as workouts + a split (201)", async () => { + const res = await request(app).post("/api/ai/save-generated-plan").set(auth(token)).send({ plan: validPlan }); + expect(res.status).toBe(201); + expect(res.body.workouts).toHaveLength(1); + expect(res.body.split.name).toBe("Test Program"); + }); + it("rejects an invalid plan (400)", async () => { + const res = await request(app).post("/api/ai/save-generated-plan").set(auth(token)).send({ plan: { nope: true } }); + expect(res.status).toBe(400); + }); + it("requires authentication (401)", async () => { + const res = await request(app).post("/api/ai/save-generated-plan").send({ plan: validPlan }); + expect(res.status).toBe(401); + }); + it("returns 500 when the LLM returns no content", async () => { + await Exercise.create({ id: 1, name: "Bench", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }); + global.fetch.mockResolvedValue(mockLLM({ content: "" })); + const res = await request(app).post("/api/ai/generate-workout").set(auth(token)); + expect(res.status).toBe(500); + }); + it("returns 500 when the LLM returns non-JSON", async () => { + await Exercise.create({ id: 1, name: "Bench", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }); + global.fetch.mockResolvedValue(mockLLM({ content: "totally not json" })); + const res = await request(app).post("/api/ai/generate-workout").set(auth(token)); + expect(res.status).toBe(500); + }); + it("returns 500 when the LLM request throws", async () => { + await Exercise.create({ id: 1, name: "Bench", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }); + global.fetch.mockRejectedValue(new Error("network down")); + const res = await request(app).post("/api/ai/generate-workout").set(auth(token)); + expect(res.status).toBe(500); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/auth.test.js b/backend/tests/auth.test.js index fbc0115..9e8e92e 100644 --- a/backend/tests/auth.test.js +++ b/backend/tests/auth.test.js @@ -1,5 +1,6 @@ const request = require("supertest"); const app = require("../app"); +const User = require("../models/User"); const validUser = { name: "Test User", email: "test@example.com", password: "password123" }; @@ -64,4 +65,57 @@ describe("Auth API", () => { expect(res.status).toBe(401); }); }); +}); + +describe("Auth API — profile & edge cases", () => { + const user = { name: "Profile User", email: "profile@example.com", password: "password123" }; + async function tokenFor() { + const res = await request(app).post("/api/auth/register").send(user); + return res.body.token; + } + + describe("PUT /api/auth/profile", () => { + it("updates the fitness profile (200)", async () => { + const token = await tokenFor(); + const res = await request(app).put("/api/auth/profile") + .set("Authorization", `Bearer ${token}`) + .send({ goal: "hypertrophy", experienceLevel: "advanced", daysPerWeek: 5 }); + expect(res.status).toBe(200); + expect(res.body.user.fitnessProfile.goal).toBe("hypertrophy"); + expect(res.body.user.fitnessProfile.daysPerWeek).toBe(5); + }); + it("requires authentication (401)", async () => { + const res = await request(app).put("/api/auth/profile").send({ goal: "strength" }); + expect(res.status).toBe(401); + }); + }); + + describe("Token validation", () => { + it("rejects /me with an invalid token (401)", async () => { + const res = await request(app).get("/api/auth/me").set("Authorization", "Bearer garbage.token"); + expect(res.status).toBe(401); + }); + it("rejects /me with a malformed header (401)", async () => { + const res = await request(app).get("/api/auth/me").set("Authorization", "NotBearer xyz"); + expect(res.status).toBe(401); + }); + it("rejects /me when the user no longer exists (401)", async () => { + const reg = await request(app).post("/api/auth/register") + .send({ name: "Gone", email: "gone@example.com", password: "password123" }); + await User.deleteMany({}); // token valid, user gone + const res = await request(app).get("/api/auth/me").set("Authorization", `Bearer ${reg.body.token}`); + expect(res.status).toBe(401); + }); + }); + + describe("Login edge cases", () => { + it("returns 401 for a non-existent email", async () => { + const res = await request(app).post("/api/auth/login").send({ email: "nobody@example.com", password: "whatever123" }); + expect(res.status).toBe(401); + }); + it("returns 400 when a field is missing", async () => { + const res = await request(app).post("/api/auth/login").send({ email: "a@b.com" }); + expect(res.status).toBe(400); + }); + }); }); \ No newline at end of file diff --git a/backend/tests/errorHandler.test.js b/backend/tests/errorHandler.test.js new file mode 100644 index 0000000..42bee3b --- /dev/null +++ b/backend/tests/errorHandler.test.js @@ -0,0 +1,28 @@ +const errorHandler = require("../middleware/errorHandler"); + +const mockRes = () => { + const res = {}; + res.status = jest.fn(() => res); + res.json = jest.fn(() => res); + return res; +}; + +describe("errorHandler", () => { + it("maps CastError to 400", () => { + const res = mockRes(); + errorHandler({ name: "CastError" }, {}, res, () => {}); + expect(res.status).toHaveBeenCalledWith(400); + }); + it("maps ValidationError to 400", () => { + const res = mockRes(); + errorHandler({ name: "ValidationError", message: "bad" }, {}, res, () => {}); + expect(res.status).toHaveBeenCalledWith(400); + }); + it("falls back to 500 for unknown errors", () => { + const spy = jest.spyOn(console, "error").mockImplementation(() => {}); + const res = mockRes(); + errorHandler({ name: "Error", message: "boom" }, {}, res, () => {}); + expect(res.status).toHaveBeenCalledWith(500); + spy.mockRestore(); + }); +}); \ No newline at end of file diff --git a/backend/tests/exercises.test.js b/backend/tests/exercises.test.js new file mode 100644 index 0000000..55ced9a --- /dev/null +++ b/backend/tests/exercises.test.js @@ -0,0 +1,28 @@ +const request = require("supertest"); +const app = require("../app"); +const Exercise = require("../models/Exercise"); + +describe("Exercises API", () => { + beforeEach(async () => { + await Exercise.insertMany([ + { id: 1, name: "Bench Press", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }, + { id: 2, name: "Squat", bodyPart: "upper legs", target: "quads", equipment: "barbell", gifUrl: "" }, + { id: 3, name: "Bicep Curl", bodyPart: "upper arms", target: "biceps", equipment: "dumbbell", gifUrl: "" }, + ]); + }); + + it("returns all exercises (200)", async () => { + const res = await request(app).get("/api/exercises"); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(3); + }); + it("filters by search term", async () => { + const res = await request(app).get("/api/exercises").query({ search: "squat" }); + expect(res.body).toHaveLength(1); + expect(res.body[0].name).toBe("Squat"); + }); + it("supports pagination via limit", async () => { + const res = await request(app).get("/api/exercises").query({ limit: 2, page: 1 }); + expect(res.body).toHaveLength(2); + }); +}); \ No newline at end of file diff --git a/backend/tests/security.test.js b/backend/tests/security.test.js index 63824a2..3250c1f 100644 --- a/backend/tests/security.test.js +++ b/backend/tests/security.test.js @@ -1,25 +1,26 @@ const request = require("supertest"); const app = require("../app"); -const { protect, adminOnly } = require("../middleware/authMiddleware"); +const { protect } = require("../middleware/authMiddleware"); describe("Security middleware", () => { - it("exports protect and adminOnly as functions", () => { + it("exports protect function", () => { expect(typeof protect).toBe("function"); - expect(typeof adminOnly).toBe("function"); }); - - it("blocks the destructive /api/exercises/fetch without auth (401)", async () => { - const res = await request(app).get("/api/exercises/fetch"); + it("rejects a protected route with no token (401)", async () => { + const res = await request(app).get("/api/workouts"); + expect(res.status).toBe(401); + }); + it("rejects a protected route with an invalid token (401)", async () => { + const res = await request(app).get("/api/workouts").set("Authorization", "Bearer not-a-real-token"); expect(res.status).toBe(401); }); + it("blocks a request from a disallowed origin (500)", async () => { + const res = await request(app).get("/api/workouts").set("Origin", "https://evil.example.com"); + expect(res.status).toBe(500); // covers app.js:27 + }); - it("blocks /api/exercises/fetch for non-admin users (403)", async () => { - const reg = await request(app) - .post("/api/auth/register") - .send({ name: "U", email: "u@e.com", password: "password123" }); - const res = await request(app) - .get("/api/exercises/fetch") - .set("Authorization", `Bearer ${reg.body.token}`); - expect(res.status).toBe(403); + it("hits the SPA catchall for non-API routes", async () => { + const res = await request(app).get("/some-frontend-page"); + expect([200, 404, 500]).toContain(res.status); // covers app.js:43 (sendFile executes) }); }); \ No newline at end of file diff --git a/backend/tests/splits.test.js b/backend/tests/splits.test.js new file mode 100644 index 0000000..476f4ad --- /dev/null +++ b/backend/tests/splits.test.js @@ -0,0 +1,124 @@ +const request = require("supertest"); +const app = require("../app"); +const Exercise = require("../models/Exercise"); + +const auth = (token) => ({ Authorization: `Bearer ${token}` }); + +async function registerUser(email) { + const res = await request(app).post("/api/auth/register") + .send({ name: "Test", email, password: "password123" }); + return res.body.token; +} +async function createWorkout(token, name, exercises = [1]) { + const res = await request(app).post("/api/workouts").set(auth(token)).send({ name, exercises }); + return res.body; +} + +describe("Splits API", () => { + let token; + beforeEach(async () => { token = await registerUser("split-user@example.com"); }); + + describe("POST /api/splits", () => { + it("creates a split (201)", async () => { + const w = await createWorkout(token, "Push"); + const res = await request(app).post("/api/splits").set(auth(token)) + .send({ name: "PPL", workouts: [w._id] }); + expect(res.status).toBe(201); + expect(res.body.name).toBe("PPL"); + }); + it("rejects a missing name (400)", async () => { + const res = await request(app).post("/api/splits").set(auth(token)).send({ workouts: [] }); + expect(res.status).toBe(400); + }); + it("rejects non-array workouts (400)", async () => { + const res = await request(app).post("/api/splits").set(auth(token)).send({ name: "Bad", workouts: "nope" }); + expect(res.status).toBe(400); + }); + it("requires authentication (401)", async () => { + const res = await request(app).post("/api/splits").send({ name: "X", workouts: [] }); + expect(res.status).toBe(401); + }); + }); + + describe("GET /api/splits", () => { + it("returns only the current user's splits", async () => { + await request(app).post("/api/splits").set(auth(token)).send({ name: "Mine", workouts: [] }); + const other = await registerUser("other-split@example.com"); + await request(app).post("/api/splits").set(auth(other)).send({ name: "Theirs", workouts: [] }); + + const res = await request(app).get("/api/splits").set(auth(token)); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(1); + expect(res.body[0].name).toBe("Mine"); + }); + }); + + describe("GET /api/splits/:id", () => { + it("fetches a split by id (success)", async () => { // NEW + const created = await request(app).post("/api/splits").set(auth(token)).send({ name: "S", workouts: [] }); + const res = await request(app).get(`/api/splits/${created.body._id}`).set(auth(token)); + expect(res.status).toBe(200); + expect(res.body.name).toBe("S"); + }); + it("returns 404 for another user's split", async () => { + const created = await request(app).post("/api/splits").set(auth(token)).send({ name: "Private", workouts: [] }); + const other = await registerUser("snoop-split@example.com"); + const res = await request(app).get(`/api/splits/${created.body._id}`).set(auth(other)); + expect(res.status).toBe(404); + }); + }); + + describe("GET /api/splits/:id/details", () => { + it("returns the split with fully populated exercise details", async () => { + await Exercise.create({ id: 1, name: "Bench Press", bodyPart: "chest", target: "pecs", equipment: "barbell", gifUrl: "" }); + const w = await createWorkout(token, "Push", [1]); + const split = await request(app).post("/api/splits").set(auth(token)).send({ name: "PPL", workouts: [w._id] }); + + const res = await request(app).get(`/api/splits/${split.body._id}/details`).set(auth(token)); + expect(res.status).toBe(200); + expect(res.body.workouts).toHaveLength(1); + expect(res.body.workouts[0].exercises[0].name).toBe("Bench Press"); + }); + it("returns 404 for a non-existent split", async () => { + const res = await request(app).get(`/api/splits/64b8f0000000000000000000/details`).set(auth(token)); + expect(res.status).toBe(404); + }); + }); + + describe("PUT /api/splits/:id", () => { + it("updates a split name", async () => { + const created = await request(app).post("/api/splits").set(auth(token)).send({ name: "Old", workouts: [] }); + const res = await request(app).put(`/api/splits/${created.body._id}`).set(auth(token)).send({ name: "New", workouts: [] }); + expect(res.status).toBe(200); + expect(res.body.name).toBe("New"); + }); + it("returns 404 when updating a non-existent split", async () => { // NEW + const res = await request(app).put("/api/splits/64b8f0000000000000000000") + .set(auth(token)).send({ name: "X", workouts: [] }); + expect(res.status).toBe(404); + }); + it("rejects an update with a missing name (400)", async () => { + const created = await request(app).post("/api/splits").set(auth(token)).send({ name: "Old", workouts: [] }); + const res = await request(app).put(`/api/splits/${created.body._id}`).set(auth(token)).send({ workouts: [] }); + expect(res.status).toBe(400); + }); + it("rejects an update with non-array workouts (400)", async () => { + const created = await request(app).post("/api/splits").set(auth(token)).send({ name: "Old", workouts: [] }); + const res = await request(app).put(`/api/splits/${created.body._id}`).set(auth(token)).send({ name: "X", workouts: "nope" }); + expect(res.status).toBe(400); + }); + }); + + describe("DELETE /api/splits/:id", () => { + it("deletes a split", async () => { + const created = await request(app).post("/api/splits").set(auth(token)).send({ name: "Temp", workouts: [] }); + expect((await request(app).delete(`/api/splits/${created.body._id}`).set(auth(token))).status).toBe(200); + const after = await request(app).get(`/api/splits/${created.body._id}`).set(auth(token)); + expect(after.status).toBe(404); + }); + it("returns 404 when deleting a non-existent split", async () => { // NEW + const res = await request(app).delete("/api/splits/64b8f0000000000000000000").set(auth(token)); + expect(res.status).toBe(404); + }); + }); +}); \ No newline at end of file diff --git a/backend/tests/workouts.test.js b/backend/tests/workouts.test.js new file mode 100644 index 0000000..25c39c0 --- /dev/null +++ b/backend/tests/workouts.test.js @@ -0,0 +1,122 @@ +const request = require("supertest"); +const app = require("../app"); + +const auth = (token) => ({ Authorization: `Bearer ${token}` }); + +async function registerUser(email) { + const res = await request(app).post("/api/auth/register") + .send({ name: "Test", email, password: "password123" }); + return res.body.token; +} + +describe("Workouts API", () => { + let token; + beforeEach(async () => { token = await registerUser("workout-user@example.com"); }); + + describe("POST /api/workouts", () => { + it("creates a workout (201)", async () => { + const res = await request(app).post("/api/workouts").set(auth(token)) + .send({ name: "Push Day", exercises: [1, 2, 3] }); + expect(res.status).toBe(201); + expect(res.body.name).toBe("Push Day"); + expect(res.body._id).toBeDefined(); // now we assert it explicitly + }); + it("rejects a missing name (400)", async () => { + const res = await request(app).post("/api/workouts").set(auth(token)).send({ exercises: [1] }); + expect(res.status).toBe(400); + }); + it("rejects non-array exercises (400)", async () => { + const res = await request(app).post("/api/workouts").set(auth(token)).send({ name: "Bad", exercises: "nope" }); + expect(res.status).toBe(400); + }); + it("rejects an empty exercises array (400)", async () => { + const res = await request(app).post("/api/workouts").set(auth(token)).send({ name: "Empty", exercises: [] }); + expect(res.status).toBe(400); // documents your "at least one exercise" rule + }); + it("requires authentication (401)", async () => { + const res = await request(app).post("/api/workouts").send({ name: "X", exercises: [1] }); + expect(res.status).toBe(401); + }); + }); + describe("GET /api/workouts", () => { + it("returns the user's workouts with populated exercises", async () => { + await request(app).post("/api/workouts").set(auth(token)).send({ name: "Mine", exercises: [1] }); + const res = await request(app).get("/api/workouts").set(auth(token)); + expect(res.status).toBe(200); + expect(res.body).toHaveLength(1); + expect(res.body[0].name).toBe("Mine"); + }); + }); + + describe("GET /api/workouts/:id", () => { + it("fetches a workout by id", async () => { + const created = await request(app).post("/api/workouts").set(auth(token)).send({ name: "One", exercises: [1] }); + const res = await request(app).get(`/api/workouts/${created.body._id}`).set(auth(token)); + expect(res.status).toBe(200); + expect(res.body.name).toBe("One"); + }); + it("returns 404 for another user's workout", async () => { + const created = await request(app).post("/api/workouts").set(auth(token)).send({ name: "Private", exercises: [1] }); + const otherToken = await registerUser("snooper@example.com"); + const res = await request(app).get(`/api/workouts/${created.body._id}`).set(auth(otherToken)); + expect(res.status).toBe(404); + }); + it("returns 400 for a malformed id", async () => { // NEW + const res = await request(app).get("/api/workouts/not-a-valid-id").set(auth(token)); + expect(res.status).toBe(400); + }); + }); + + describe("GET /api/workouts/:id", () => { + it("fetches a workout by id", async () => { + const created = await request(app).post("/api/workouts").set(auth(token)).send({ name: "One", exercises: [1] }); + const res = await request(app).get(`/api/workouts/${created.body._id}`).set(auth(token)); + expect(res.status).toBe(200); + expect(res.body.name).toBe("One"); + }); + it("returns 404 for another user's workout", async () => { + const created = await request(app).post("/api/workouts").set(auth(token)).send({ name: "Private", exercises: [1] }); + const otherToken = await registerUser("snooper@example.com"); + const res = await request(app).get(`/api/workouts/${created.body._id}`).set(auth(otherToken)); + expect(res.status).toBe(404); + }); + }); + + describe("PUT /api/workouts/:id", () => { + it("updates a workout", async () => { + const created = await request(app).post("/api/workouts").set(auth(token)).send({ name: "Old", exercises: [1] }); + const res = await request(app).put(`/api/workouts/${created.body._id}`).set(auth(token)) + .send({ name: "New", exercises: [5] }); + expect(res.status).toBe(200); + expect(res.body.name).toBe("New"); + }); + it("returns 404 when updating a non-existent workout", async () => { // NEW + const res = await request(app).put("/api/workouts/64b8f0000000000000000000") + .set(auth(token)).send({ name: "X", exercises: [1] }); + expect(res.status).toBe(404); + }); + it("rejects an update with a missing name (400)", async () => { + const created = await request(app).post("/api/workouts").set(auth(token)).send({ name: "Old", exercises: [1] }); + const res = await request(app).put(`/api/workouts/${created.body._id}`).set(auth(token)).send({ exercises: [1] }); + expect(res.status).toBe(400); + }); + it("rejects an update with empty exercises (400)", async () => { + const created = await request(app).post("/api/workouts").set(auth(token)).send({ name: "Old", exercises: [1] }); + const res = await request(app).put(`/api/workouts/${created.body._id}`).set(auth(token)).send({ name: "X", exercises: [] }); + expect(res.status).toBe(400); + }); + }); + + describe("DELETE /api/workouts/:id", () => { + it("deletes a workout", async () => { + const created = await request(app).post("/api/workouts").set(auth(token)).send({ name: "Temp", exercises: [1] }); + expect((await request(app).delete(`/api/workouts/${created.body._id}`).set(auth(token))).status).toBe(200); + const after = await request(app).get(`/api/workouts/${created.body._id}`).set(auth(token)); + expect(after.status).toBe(404); + }); + it("returns 404 when deleting a non-existent workout", async () => { // NEW + const res = await request(app).delete("/api/workouts/64b8f0000000000000000000").set(auth(token)); + expect(res.status).toBe(404); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/apis/aiApi.test.js b/frontend/src/apis/aiApi.test.js new file mode 100644 index 0000000..e824a60 --- /dev/null +++ b/frontend/src/apis/aiApi.test.js @@ -0,0 +1,41 @@ +import { generateWorkoutPlan, saveGeneratedPlan } from "./aiApi"; + +const ok = (body) => ({ ok: true, json: async () => body }); +const fail = (message) => ({ ok: false, json: async () => ({ message }) }); + +describe("aiApi", () => { + beforeEach(() => { + global.fetch = jest.fn(); + localStorage.setItem("fitforgeToken", "test-token"); + }); + afterEach(() => { jest.resetAllMocks(); localStorage.clear(); }); + + it("generateWorkoutPlan returns the plan (unwraps data.plan)", async () => { + global.fetch.mockResolvedValue(ok({ plan: { programName: "P" } })); + const plan = await generateWorkoutPlan(); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/ai/generate-workout"), + expect.objectContaining({ method: "POST" }) + ); + expect(plan.programName).toBe("P"); + }); + + it("saveGeneratedPlan POSTs the plan wrapped in { plan }", async () => { + global.fetch.mockResolvedValue(ok({ message: "saved" })); + const res = await saveGeneratedPlan({ programName: "P", days: [] }); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/ai/save-generated-plan"), + expect.objectContaining({ method: "POST", body: JSON.stringify({ plan: { programName: "P", days: [] } }) }) + ); + expect(res.message).toBe("saved"); + }); + + it("throws the server message on failure", async () => { + global.fetch.mockResolvedValue(fail("Failed to generate workout plan")); + await expect(generateWorkoutPlan()).rejects.toThrow("Failed to generate workout plan"); + }); + it("saveGeneratedPlan throws on a non-ok response", async () => { + global.fetch.mockResolvedValue(fail("Failed to save generated plan")); + await expect(saveGeneratedPlan({ days: [] })).rejects.toThrow("Failed to save generated plan"); + }); +}); \ No newline at end of file diff --git a/frontend/src/apis/authApi.test.js b/frontend/src/apis/authApi.test.js new file mode 100644 index 0000000..5a43c96 --- /dev/null +++ b/frontend/src/apis/authApi.test.js @@ -0,0 +1,51 @@ +import { registerUser, loginUser, getCurrentUser } from "./authApi"; + +const ok = (body) => ({ ok: true, json: async () => body }); +const fail = (message) => ({ ok: false, json: async () => ({ message }) }); + +describe("authApi", () => { + beforeEach(() => { global.fetch = jest.fn(); }); + afterEach(() => jest.resetAllMocks()); + + it("registerUser POSTs credentials and returns the token payload", async () => { + global.fetch.mockResolvedValue(ok({ token: "t", user: { email: "a@b.com" } })); + const data = await registerUser({ name: "A", email: "a@b.com", password: "password123" }); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/auth/register"), + expect.objectContaining({ method: "POST" }) + ); + expect(data.token).toBe("t"); + }); + + it("loginUser POSTs to /auth/login", async () => { + global.fetch.mockResolvedValue(ok({ token: "t" })); + const data = await loginUser({ email: "a@b.com", password: "password123" }); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/auth/login"), + expect.objectContaining({ method: "POST" }) + ); + expect(data.token).toBe("t"); + }); + + it("getCurrentUser sends the bearer token", async () => { + global.fetch.mockResolvedValue(ok({ user: { email: "a@b.com" } })); + const data = await getCurrentUser("abc"); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/auth/me"), + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer abc" }) }) + ); + expect(data.user.email).toBe("a@b.com"); + }); + + it("throws the server message on failure", async () => { + global.fetch.mockResolvedValue(fail("Invalid email or password")); + await expect(loginUser({ email: "x", password: "y" })).rejects.toThrow("Invalid email or password"); + }); + it.each([ + ["registerUser", () => registerUser({ name: "a", email: "a@b.com", password: "x" })], + ["getCurrentUser", () => getCurrentUser("tok")], + ])("%s throws on a non-ok response", async (_n, call) => { + global.fetch.mockResolvedValue(fail("boom")); + await expect(call()).rejects.toThrow("boom"); + }); +}); \ No newline at end of file diff --git a/frontend/src/apis/authHeaders.test.js b/frontend/src/apis/authHeaders.test.js new file mode 100644 index 0000000..0c4e360 --- /dev/null +++ b/frontend/src/apis/authHeaders.test.js @@ -0,0 +1,15 @@ +import { getAuthHeaders } from "./authHeaders"; + +describe("getAuthHeaders", () => { + afterEach(() => localStorage.clear()); + + it("returns an Authorization header when a token exists", () => { + localStorage.setItem("fitforgeToken", "abc123"); + expect(getAuthHeaders()).toEqual({ Authorization: "Bearer abc123" }); + }); + + it("returns an empty object when there is no token", () => { + localStorage.removeItem("fitforgeToken"); + expect(getAuthHeaders()).toEqual({}); + }); +}); \ No newline at end of file diff --git a/frontend/src/apis/exerciseApi.test.js b/frontend/src/apis/exerciseApi.test.js new file mode 100644 index 0000000..19c17ad --- /dev/null +++ b/frontend/src/apis/exerciseApi.test.js @@ -0,0 +1,34 @@ +import axios from "axios"; +import { fetchExercises, triggerExerciseFetch } from "./exerciseApi"; + +jest.mock("axios"); + +describe("exerciseApi", () => { + afterEach(() => jest.resetAllMocks()); + + it("fetchExercises GETs /exercises with search + pagination params", async () => { + axios.get.mockResolvedValue({ data: [{ id: 1, name: "Bench" }] }); + const data = await fetchExercises("bench", 1, 24); + expect(axios.get).toHaveBeenCalledWith( + expect.stringContaining("/exercises"), + expect.objectContaining({ params: { search: "bench", page: 1, limit: 24 } }) + ); + expect(data).toEqual([{ id: 1, name: "Bench" }]); + }); + + it("triggerExerciseFetch GETs /exercises/fetch", async () => { + axios.get.mockResolvedValue({ data: { message: "ok" } }); + const data = await triggerExerciseFetch(); + expect(axios.get).toHaveBeenCalledWith(expect.stringContaining("/exercises/fetch")); + expect(data.message).toBe("ok"); + }); + + it("fetchExercises uses default params when called with none", async () => { + axios.get.mockResolvedValue({ data: [] }); + await fetchExercises(); + expect(axios.get).toHaveBeenCalledWith( + expect.stringContaining("/exercises"), + expect.objectContaining({ params: { search: "", page: 1, limit: 1500 } }) + ); + }); +}); \ No newline at end of file diff --git a/frontend/src/apis/splitApi.js b/frontend/src/apis/splitApi.js index 13d1bc7..17705fb 100644 --- a/frontend/src/apis/splitApi.js +++ b/frontend/src/apis/splitApi.js @@ -53,6 +53,17 @@ export const fetchSplitById = async (id) => { return data; }; +export const fetchSplitDetails = async (id) => { + const response = await fetch(`${API_URL}/splits/${id}/details`, { + headers: { ...getAuthHeaders() }, + }); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message || "Failed to fetch split details"); + } + return data; +}; + export const updateSplit = async (id, split) => { const response = await fetch(`${API_URL}/splits/${id}`, { method: "PUT", diff --git a/frontend/src/apis/splitApi.test.js b/frontend/src/apis/splitApi.test.js new file mode 100644 index 0000000..8156f80 --- /dev/null +++ b/frontend/src/apis/splitApi.test.js @@ -0,0 +1,75 @@ +import { createSplit, fetchSplits, fetchSplitById, updateSplit, deleteSplit } from "./splitApi"; + +const ok = (body) => ({ ok: true, json: async () => body }); +const fail = (message) => ({ ok: false, json: async () => ({ message }) }); + +describe("splitApi", () => { + beforeEach(() => { + global.fetch = jest.fn(); + localStorage.setItem("fitforgeToken", "test-token"); + }); + afterEach(() => { jest.resetAllMocks(); localStorage.clear(); }); + + it("fetchSplits returns data and sends the auth header", async () => { + global.fetch.mockResolvedValue(ok([{ _id: "1", name: "PPL" }])); + const data = await fetchSplits(); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/splits"), + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer test-token" }) }) + ); + expect(data).toEqual([{ _id: "1", name: "PPL" }]); + }); + + it("createSplit POSTs the body", async () => { + global.fetch.mockResolvedValue(ok({ _id: "2", name: "New" })); + const res = await createSplit({ name: "New", workouts: [] }); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/splits"), + expect.objectContaining({ method: "POST", body: JSON.stringify({ name: "New", workouts: [] }) }) + ); + expect(res.name).toBe("New"); + }); + + it("fetchSplitById requests the id", async () => { + global.fetch.mockResolvedValue(ok({ _id: "9", name: "X" })); + const res = await fetchSplitById("9"); + expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining("/splits/9"), expect.any(Object)); + expect(res._id).toBe("9"); + }); + + it("updateSplit PUTs to the id", async () => { + global.fetch.mockResolvedValue(ok({ _id: "9", name: "Upd" })); + const res = await updateSplit("9", { name: "Upd", workouts: [] }); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/splits/9"), + expect.objectContaining({ method: "PUT" }) + ); + expect(res.name).toBe("Upd"); + }); + + it("deleteSplit DELETEs the id", async () => { + global.fetch.mockResolvedValue(ok({ message: "deleted" })); + const res = await deleteSplit("9"); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/splits/9"), + expect.objectContaining({ method: "DELETE" }) + ); + expect(res.message).toBe("deleted"); + }); + + it("throws the server message on a non-ok response", async () => { + global.fetch.mockResolvedValue(fail("Failed to fetch splits")); + await expect(fetchSplits()).rejects.toThrow("Failed to fetch splits"); + }); + + + it.each([ + ["createSplit", () => createSplit({ name: "x", workouts: [] })], + ["fetchSplitById", () => fetchSplitById("1")], + ["updateSplit", () => updateSplit("1", { name: "x", workouts: [] })], + ["deleteSplit", () => deleteSplit("1")], + ])("%s throws on a non-ok response", async (_n, call) => { + global.fetch.mockResolvedValue(fail("boom")); + await expect(call()).rejects.toThrow("boom"); + }); +}); \ No newline at end of file diff --git a/frontend/src/apis/workoutApi.test.js b/frontend/src/apis/workoutApi.test.js new file mode 100644 index 0000000..5b5a8f4 --- /dev/null +++ b/frontend/src/apis/workoutApi.test.js @@ -0,0 +1,73 @@ +import { fetchWorkouts, createWorkout, fetchWorkoutById, updateWorkout, deleteWorkout } from "./workoutApi"; + +const ok = (body) => ({ ok: true, json: async () => body }); +const fail = (message) => ({ ok: false, json: async () => ({ message }) }); + +describe("workoutApi", () => { + beforeEach(() => { + global.fetch = jest.fn(); + localStorage.setItem("fitforgeToken", "test-token"); + }); + afterEach(() => { jest.resetAllMocks(); localStorage.clear(); }); + + it("fetchWorkouts returns data with the auth header", async () => { + global.fetch.mockResolvedValue(ok([{ _id: "1", name: "Push" }])); + const data = await fetchWorkouts(); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/workouts"), + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer test-token" }) }) + ); + expect(data).toHaveLength(1); + }); + + it("createWorkout POSTs the body", async () => { + global.fetch.mockResolvedValue(ok({ _id: "2", name: "New" })); + const res = await createWorkout({ name: "New", exercises: [1] }); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/workouts"), + expect.objectContaining({ method: "POST" }) + ); + expect(res.name).toBe("New"); + }); + + it("fetchWorkoutById requests the id", async () => { + global.fetch.mockResolvedValue(ok({ _id: "9" })); + const res = await fetchWorkoutById("9"); + expect(global.fetch).toHaveBeenCalledWith(expect.stringContaining("/workouts/9"), expect.any(Object)); + expect(res._id).toBe("9"); + }); + + it("updateWorkout PUTs to the id", async () => { + global.fetch.mockResolvedValue(ok({ _id: "9", name: "Upd" })); + const res = await updateWorkout("9", { name: "Upd", exercises: [1] }); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/workouts/9"), + expect.objectContaining({ method: "PUT" }) + ); + expect(res.name).toBe("Upd"); + }); + + it("deleteWorkout DELETEs the id", async () => { + global.fetch.mockResolvedValue(ok({ message: "deleted" })); + const res = await deleteWorkout("9"); + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/workouts/9"), + expect.objectContaining({ method: "DELETE" }) + ); + expect(res.message).toBe("deleted"); + }); + + it("throws on a non-ok response", async () => { + global.fetch.mockResolvedValue(fail("Failed to fetch workouts")); + await expect(fetchWorkouts()).rejects.toThrow("Failed to fetch workouts"); + }); + it.each([ + ["createWorkout", () => createWorkout({ name: "x", exercises: [1] })], + ["fetchWorkoutById", () => fetchWorkoutById("1")], + ["updateWorkout", () => updateWorkout("1", { name: "x", exercises: [1] })], + ["deleteWorkout", () => deleteWorkout("1")], + ])("%s throws on a non-ok response", async (_n, call) => { + global.fetch.mockResolvedValue(fail("boom")); + await expect(call()).rejects.toThrow("boom"); + }); +}); \ No newline at end of file diff --git a/frontend/src/components/SplitLibrary/SplitLibrary.css b/frontend/src/components/SplitLibrary/SplitLibrary.css index 443c5d6..c96be61 100644 --- a/frontend/src/components/SplitLibrary/SplitLibrary.css +++ b/frontend/src/components/SplitLibrary/SplitLibrary.css @@ -6,11 +6,13 @@ text-align: center; } -.split-list { - display: flex; - flex-wrap: wrap; - justify-content: center; +.splits-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; + width: 100%; + max-width: 1000px; + margin: 20px auto; } .split-box { @@ -18,10 +20,11 @@ border: 1px solid #ccc; border-radius: 10px; padding: 20px; - width: 300px; + width: auto; /* was 300px — let the grid size it */ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); text-align: left; - transition: box-shadow 0.3s; + cursor: pointer; /* signal it's clickable */ + transition: box-shadow 0.3s, transform 0.3s; background-color: #f9f9f9; } @@ -105,3 +108,56 @@ .create-split-button:hover { background-color: #8E44AD; } + +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; +} + +.modal-card { + background: #fff; + border-radius: 12px; + padding: 30px; + width: 100%; + max-width: 500px; + max-height: 80vh; + overflow-y: auto; + position: relative; + text-align: left; +} + +.modal-close { + position: absolute; + top: 12px; + right: 16px; + border: none; + background: transparent; + font-size: 28px; + line-height: 1; + cursor: pointer; +} + +.modal-workout { margin-bottom: 20px; } +.modal-workout h3 { + margin: 0 0 8px 0; + border-bottom: 1px solid #eee; + padding-bottom: 4px; +} +.modal-exercise-list { list-style: none; padding: 0; margin: 0; } +.modal-exercise-list li { + padding: 8px 0; + border-bottom: 1px solid #f0f0f0; + display: flex; + flex-direction: column; +} +.exercise-meta { + font-size: 0.85rem; + color: #777; + text-transform: capitalize; +} \ No newline at end of file diff --git a/frontend/src/components/SplitLibrary/SplitLibrary.js b/frontend/src/components/SplitLibrary/SplitLibrary.js index 7a858ef..9b5f2ea 100644 --- a/frontend/src/components/SplitLibrary/SplitLibrary.js +++ b/frontend/src/components/SplitLibrary/SplitLibrary.js @@ -1,19 +1,21 @@ import React, { useEffect, useState } from "react"; -import { fetchSplits, deleteSplit } from "../../apis/splitApi"; // Ensure these are correctly implemented in splitApi.js -import { Link, useNavigate } from "react-router-dom"; // Import useNavigate -import "./SplitLibrary.css"; // Import CSS for styling +import { fetchSplits, deleteSplit, fetchSplitDetails } from "../../apis/splitApi"; +import { Link, useNavigate } from "react-router-dom"; +import "./SplitLibrary.css"; const SplitLibrary = () => { const [splits, setSplits] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const navigate = useNavigate(); // Initialize useNavigate + const [selectedSplit, setSelectedSplit] = useState(null); + const [detailsLoading, setDetailsLoading] = useState(false); + const navigate = useNavigate(); useEffect(() => { const getSplits = async () => { try { - const fetchedSplits = await fetchSplits(); // Fetch all splits + const fetchedSplits = await fetchSplits(); setSplits(fetchedSplits); } catch (err) { setError(err.message); @@ -21,27 +23,23 @@ const SplitLibrary = () => { setLoading(false); } }; - getSplits(); }, []); - // Handle delete function for a split - const handleDeleteSplit = async (id) => { + const handleDeleteSplit = async (e, id) => { + e.stopPropagation(); // don't open the modal when deleting try { - await deleteSplit(id); // Delete the split by ID - setSplits(splits.filter((split) => split._id !== id)); // Remove the deleted split from state + await deleteSplit(id); + setSplits(splits.filter((split) => split._id !== id)); } catch (err) { console.error("Error deleting split:", err); } }; - // Handle search input change - const handleSearchChange = (e) => { - setSearchTerm(e.target.value); - }; + const handleSearchChange = (e) => setSearchTerm(e.target.value); - // Handle programmatic navigation for editing split - const handleEditSplit = (split) => { + const handleEditSplit = (e, split) => { + e.stopPropagation(); // don't open the modal when editing navigate("/split-planner", { state: { splitId: split._id, @@ -51,7 +49,21 @@ const SplitLibrary = () => { }); }; - // Filter splits based on search term + const handleOpenSplit = async (split) => { + setSelectedSplit(split); // show name immediately + setDetailsLoading(true); + try { + const detailed = await fetchSplitDetails(split._id); + setSelectedSplit(detailed); // swap in full exercise details + } catch (err) { + console.error("Error loading split details:", err); + } finally { + setDetailsLoading(false); + } + }; + + const handleCloseModal = () => setSelectedSplit(null); + const filteredSplits = splits.filter((split) => split.name.toLowerCase().includes(searchTerm.toLowerCase()) ); @@ -74,7 +86,11 @@ const SplitLibrary = () => {

No splits found

) : ( filteredSplits.map((split) => ( -
+
handleOpenSplit(split)} + >

{split.name}

Workouts:

{split.workouts.length === 0 ? ( @@ -86,18 +102,15 @@ const SplitLibrary = () => { ))} )} -
)) @@ -106,8 +119,42 @@ const SplitLibrary = () => { Create Split + + {selectedSplit && ( +
+
e.stopPropagation()}> + +

{selectedSplit.name}

+ {detailsLoading ? ( +

Loading exercises…

+ ) : ( + selectedSplit.workouts.map((workout) => ( +
+

{workout.name}

+ {workout.exercises && workout.exercises.length > 0 ? ( +
    + {workout.exercises.map((ex) => ( +
  • + {ex.name} + + {[ex.bodyPart, ex.target, ex.equipment].filter(Boolean).join(" · ")} + +
  • + ))} +
+ ) : ( +

No exercises in this workout

+ )} +
+ )) + )} +
+
+ )}
); }; -export default SplitLibrary; +export default SplitLibrary; \ No newline at end of file diff --git a/frontend/src/components/SplitLibrary/SplitLibrary.test.js b/frontend/src/components/SplitLibrary/SplitLibrary.test.js new file mode 100644 index 0000000..d7adf5d --- /dev/null +++ b/frontend/src/components/SplitLibrary/SplitLibrary.test.js @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react"; +import { MemoryRouter } from "react-router-dom"; +import SplitLibrary from "./SplitLibrary"; +import { fetchSplits } from "../../apis/splitApi"; + +jest.mock("../../apis/splitApi"); + +const renderWithRouter = (ui) => render({ui}); + +describe("SplitLibrary", () => { + beforeEach(() => jest.clearAllMocks()); + + it("renders splits returned by the API", async () => { + fetchSplits.mockResolvedValue([ + { _id: "1", name: "Push Pull Legs", workouts: [{ _id: "w1", name: "Push" }] }, + ]); + renderWithRouter(); + expect(await screen.findByText("Push Pull Legs")).toBeInTheDocument(); + expect(screen.getByText("Push")).toBeInTheDocument(); + }); + + it("shows 'No splits found' when there are none", async () => { + fetchSplits.mockResolvedValue([]); + renderWithRouter(); + expect(await screen.findByText(/no splits found/i)).toBeInTheDocument(); + }); + + it("shows an error message if fetching fails", async () => { + fetchSplits.mockRejectedValue(new Error("Network down")); + renderWithRouter(); + expect(await screen.findByText(/network down/i)).toBeInTheDocument(); + }); +}); \ No newline at end of file diff --git a/frontend/src/context/AuthContext.test.js b/frontend/src/context/AuthContext.test.js new file mode 100644 index 0000000..0548c63 --- /dev/null +++ b/frontend/src/context/AuthContext.test.js @@ -0,0 +1,22 @@ +import { render, screen } from "@testing-library/react"; +import { AuthProvider, useAuth } from "./AuthContext"; + +const Consumer = () => { + const { isAuthenticated, user } = useAuth(); + return
{isAuthenticated ? `Hi ${user.name}` : "Logged out"}
; +}; + +describe("AuthContext", () => { + beforeEach(() => localStorage.clear()); + + it("starts logged out when there is no token", () => { + render(); + expect(screen.getByText("Logged out")).toBeInTheDocument(); + }); + + it("throws if useAuth is used outside an AuthProvider", () => { + const spy = jest.spyOn(console, "error").mockImplementation(() => {}); + expect(() => render()).toThrow(/useAuth must be used within an AuthProvider/); + spy.mockRestore(); + }); +}); \ No newline at end of file From ddab1a9db51d8aa61af314c0a2964ac73831b4b1 Mon Sep 17 00:00:00 2001 From: Nathan Huang Date: Sat, 6 Jun 2026 16:29:15 -0700 Subject: [PATCH 2/2] Stop tracking generated coverage reports --- .gitignore | 3 +- backend/coverage/clover.xml | 332 -------- backend/coverage/coverage-final.json | 14 - .../coverage/lcov-report/backend/app.js.html | 229 ------ .../coverage/lcov-report/backend/index.html | 116 --- .../backend/middleware/authMiddleware.js.html | 151 ---- .../backend/middleware/errorHandler.js.html | 121 --- .../lcov-report/backend/middleware/index.html | 146 ---- .../backend/middleware/rateLimiters.js.html | 181 ----- .../backend/models/Exercise.js.html | 121 --- .../lcov-report/backend/models/Split.js.html | 160 ---- .../lcov-report/backend/models/User.js.html | 241 ------ .../backend/models/Workout.js.html | 160 ---- .../lcov-report/backend/models/index.html | 161 ---- .../lcov-report/backend/routes/ai.js.html | 724 ------------------ .../lcov-report/backend/routes/auth.js.html | 436 ----------- .../backend/routes/exercises.js.html | 181 ----- .../lcov-report/backend/routes/index.html | 176 ----- .../lcov-report/backend/routes/splits.js.html | 472 ------------ .../backend/routes/workouts.js.html | 436 ----------- backend/coverage/lcov-report/base.css | 224 ------ .../coverage/lcov-report/block-navigation.js | 87 --- backend/coverage/lcov-report/favicon.png | Bin 445 -> 0 bytes backend/coverage/lcov-report/index.html | 161 ---- backend/coverage/lcov-report/prettify.css | 1 - backend/coverage/lcov-report/prettify.js | 2 - .../lcov-report/sort-arrow-sprite.png | Bin 138 -> 0 bytes backend/coverage/lcov-report/sorter.js | 210 ----- backend/coverage/lcov.info | 597 --------------- 29 files changed, 2 insertions(+), 5841 deletions(-) delete mode 100644 backend/coverage/clover.xml delete mode 100644 backend/coverage/coverage-final.json delete mode 100644 backend/coverage/lcov-report/backend/app.js.html delete mode 100644 backend/coverage/lcov-report/backend/index.html delete mode 100644 backend/coverage/lcov-report/backend/middleware/authMiddleware.js.html delete mode 100644 backend/coverage/lcov-report/backend/middleware/errorHandler.js.html delete mode 100644 backend/coverage/lcov-report/backend/middleware/index.html delete mode 100644 backend/coverage/lcov-report/backend/middleware/rateLimiters.js.html delete mode 100644 backend/coverage/lcov-report/backend/models/Exercise.js.html delete mode 100644 backend/coverage/lcov-report/backend/models/Split.js.html delete mode 100644 backend/coverage/lcov-report/backend/models/User.js.html delete mode 100644 backend/coverage/lcov-report/backend/models/Workout.js.html delete mode 100644 backend/coverage/lcov-report/backend/models/index.html delete mode 100644 backend/coverage/lcov-report/backend/routes/ai.js.html delete mode 100644 backend/coverage/lcov-report/backend/routes/auth.js.html delete mode 100644 backend/coverage/lcov-report/backend/routes/exercises.js.html delete mode 100644 backend/coverage/lcov-report/backend/routes/index.html delete mode 100644 backend/coverage/lcov-report/backend/routes/splits.js.html delete mode 100644 backend/coverage/lcov-report/backend/routes/workouts.js.html delete mode 100644 backend/coverage/lcov-report/base.css delete mode 100644 backend/coverage/lcov-report/block-navigation.js delete mode 100644 backend/coverage/lcov-report/favicon.png delete mode 100644 backend/coverage/lcov-report/index.html delete mode 100644 backend/coverage/lcov-report/prettify.css delete mode 100644 backend/coverage/lcov-report/prettify.js delete mode 100644 backend/coverage/lcov-report/sort-arrow-sprite.png delete mode 100644 backend/coverage/lcov-report/sorter.js delete mode 100644 backend/coverage/lcov.info diff --git a/.gitignore b/.gitignore index c28c212..21c06a4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ node_modules/ # testing /coverage +coverage/ # build outputs build/ @@ -32,4 +33,4 @@ pnpm-debug.log* # OS/editor .DS_Store -.vscode/ \ No newline at end of file +.vscode/ diff --git a/backend/coverage/clover.xml b/backend/coverage/clover.xml deleted file mode 100644 index 3276ca5..0000000 --- a/backend/coverage/clover.xml +++ /dev/null @@ -1,332 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/backend/coverage/coverage-final.json b/backend/coverage/coverage-final.json deleted file mode 100644 index eb10fe9..0000000 --- a/backend/coverage/coverage-final.json +++ /dev/null @@ -1,14 +0,0 @@ -{"/mnt/c/users/Nathan Huang/FitForge/backend/app.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/app.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":13},"end":{"line":2,"column":28}},"2":{"start":{"line":3,"column":15},"end":{"line":3,"column":32}},"3":{"start":{"line":4,"column":19},"end":{"line":4,"column":41}},"4":{"start":{"line":5,"column":13},"end":{"line":5,"column":28}},"5":{"start":{"line":6,"column":0},"end":{"line":6,"column":32}},"6":{"start":{"line":8,"column":23},"end":{"line":8,"column":52}},"7":{"start":{"line":9,"column":22},"end":{"line":9,"column":50}},"8":{"start":{"line":10,"column":20},"end":{"line":10,"column":46}},"9":{"start":{"line":11,"column":19},"end":{"line":11,"column":43}},"10":{"start":{"line":12,"column":17},"end":{"line":12,"column":39}},"11":{"start":{"line":13,"column":47},"end":{"line":13,"column":83}},"12":{"start":{"line":15,"column":12},"end":{"line":15,"column":21}},"13":{"start":{"line":17,"column":23},"end":{"line":20,"column":18}},"14":{"start":{"line":19,"column":19},"end":{"line":19,"column":32}},"15":{"start":{"line":22,"column":0},"end":{"line":22,"column":50}},"16":{"start":{"line":23,"column":0},"end":{"line":30,"column":2}},"17":{"start":{"line":26,"column":6},"end":{"line":26,"column":82}},"18":{"start":{"line":26,"column":54},"end":{"line":26,"column":82}},"19":{"start":{"line":27,"column":6},"end":{"line":27,"column":56}},"20":{"start":{"line":31,"column":0},"end":{"line":31,"column":43}},"21":{"start":{"line":33,"column":0},"end":{"line":33,"column":28}},"22":{"start":{"line":34,"column":0},"end":{"line":34,"column":42}},"23":{"start":{"line":35,"column":0},"end":{"line":35,"column":40}},"24":{"start":{"line":36,"column":0},"end":{"line":36,"column":36}},"25":{"start":{"line":37,"column":0},"end":{"line":37,"column":46}},"26":{"start":{"line":38,"column":0},"end":{"line":38,"column":40}},"27":{"start":{"line":41,"column":0},"end":{"line":41,"column":67}},"28":{"start":{"line":42,"column":0},"end":{"line":44,"column":3}},"29":{"start":{"line":43,"column":2},"end":{"line":43,"column":72}},"30":{"start":{"line":46,"column":21},"end":{"line":46,"column":57}},"31":{"start":{"line":47,"column":0},"end":{"line":47,"column":22}},"32":{"start":{"line":49,"column":0},"end":{"line":49,"column":21}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":19,"column":7},"end":{"line":19,"column":8}},"loc":{"start":{"line":19,"column":19},"end":{"line":19,"column":32}},"line":19},"1":{"name":"(anonymous_1)","decl":{"start":{"line":25,"column":12},"end":{"line":25,"column":13}},"loc":{"start":{"line":25,"column":34},"end":{"line":28,"column":5}},"line":25},"2":{"name":"(anonymous_2)","decl":{"start":{"line":42,"column":13},"end":{"line":42,"column":14}},"loc":{"start":{"line":42,"column":27},"end":{"line":44,"column":1}},"line":42}},"branchMap":{"0":{"loc":{"start":{"line":17,"column":24},"end":{"line":17,"column":74}},"type":"binary-expr","locations":[{"start":{"line":17,"column":24},"end":{"line":17,"column":47}},{"start":{"line":17,"column":51},"end":{"line":17,"column":74}}],"line":17},"1":{"loc":{"start":{"line":26,"column":6},"end":{"line":26,"column":82}},"type":"if","locations":[{"start":{"line":26,"column":6},"end":{"line":26,"column":82}},{"start":{},"end":{}}],"line":26},"2":{"loc":{"start":{"line":26,"column":10},"end":{"line":26,"column":52}},"type":"binary-expr","locations":[{"start":{"line":26,"column":10},"end":{"line":26,"column":17}},{"start":{"line":26,"column":21},"end":{"line":26,"column":52}}],"line":26}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":6,"8":6,"9":6,"10":6,"11":6,"12":6,"13":6,"14":6,"15":6,"16":6,"17":143,"18":142,"19":1,"20":6,"21":6,"22":6,"23":6,"24":6,"25":6,"26":6,"27":6,"28":6,"29":1,"30":6,"31":6,"32":6},"f":{"0":6,"1":143,"2":1},"b":{"0":[6,6],"1":[142,1],"2":[143,1]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"1e5990567cf2a5149fed8bfe4eb251114ba02ae8"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/authMiddleware.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/authMiddleware.js","statementMap":{"0":{"start":{"line":1,"column":12},"end":{"line":1,"column":35}},"1":{"start":{"line":2,"column":13},"end":{"line":2,"column":38}},"2":{"start":{"line":4,"column":16},"end":{"line":21,"column":1}},"3":{"start":{"line":5,"column":2},"end":{"line":20,"column":3}},"4":{"start":{"line":6,"column":23},"end":{"line":6,"column":48}},"5":{"start":{"line":7,"column":4},"end":{"line":9,"column":5}},"6":{"start":{"line":8,"column":6},"end":{"line":8,"column":75}},"7":{"start":{"line":10,"column":18},"end":{"line":10,"column":42}},"8":{"start":{"line":11,"column":20},"end":{"line":11,"column":61}},"9":{"start":{"line":12,"column":17},"end":{"line":12,"column":76}},"10":{"start":{"line":13,"column":4},"end":{"line":15,"column":5}},"11":{"start":{"line":14,"column":6},"end":{"line":14,"column":81}},"12":{"start":{"line":16,"column":4},"end":{"line":16,"column":20}},"13":{"start":{"line":17,"column":4},"end":{"line":17,"column":11}},"14":{"start":{"line":19,"column":4},"end":{"line":19,"column":77}},"15":{"start":{"line":23,"column":0},"end":{"line":23,"column":29}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":4,"column":16},"end":{"line":4,"column":17}},"loc":{"start":{"line":4,"column":42},"end":{"line":21,"column":1}},"line":4}},"branchMap":{"0":{"loc":{"start":{"line":7,"column":4},"end":{"line":9,"column":5}},"type":"if","locations":[{"start":{"line":7,"column":4},"end":{"line":9,"column":5}},{"start":{},"end":{}}],"line":7},"1":{"loc":{"start":{"line":7,"column":8},"end":{"line":7,"column":56}},"type":"binary-expr","locations":[{"start":{"line":7,"column":8},"end":{"line":7,"column":19}},{"start":{"line":7,"column":23},"end":{"line":7,"column":56}}],"line":7},"2":{"loc":{"start":{"line":13,"column":4},"end":{"line":15,"column":5}},"type":"if","locations":[{"start":{"line":13,"column":4},"end":{"line":15,"column":5}},{"start":{},"end":{}}],"line":13}},"s":{"0":6,"1":6,"2":6,"3":76,"4":76,"5":76,"6":8,"7":68,"8":68,"9":66,"10":66,"11":1,"12":65,"13":65,"14":2,"15":6},"f":{"0":76},"b":{"0":[8,68],"1":[76,69],"2":[1,65]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"ae932a65be686fc48b0466be89a122e6462672f7"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/errorHandler.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/errorHandler.js","statementMap":{"0":{"start":{"line":2,"column":21},"end":{"line":11,"column":1}},"1":{"start":{"line":3,"column":2},"end":{"line":5,"column":3}},"2":{"start":{"line":4,"column":4},"end":{"line":4,"column":66}},"3":{"start":{"line":6,"column":2},"end":{"line":8,"column":3}},"4":{"start":{"line":7,"column":4},"end":{"line":7,"column":58}},"5":{"start":{"line":9,"column":2},"end":{"line":9,"column":49}},"6":{"start":{"line":10,"column":2},"end":{"line":10,"column":52}},"7":{"start":{"line":13,"column":0},"end":{"line":13,"column":30}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":2,"column":21},"end":{"line":2,"column":22}},"loc":{"start":{"line":2,"column":46},"end":{"line":11,"column":1}},"line":2}},"branchMap":{"0":{"loc":{"start":{"line":3,"column":2},"end":{"line":5,"column":3}},"type":"if","locations":[{"start":{"line":3,"column":2},"end":{"line":5,"column":3}},{"start":{},"end":{}}],"line":3},"1":{"loc":{"start":{"line":6,"column":2},"end":{"line":8,"column":3}},"type":"if","locations":[{"start":{"line":6,"column":2},"end":{"line":8,"column":3}},{"start":{},"end":{}}],"line":6}},"s":{"0":7,"1":7,"2":2,"3":5,"4":1,"5":4,"6":4,"7":7},"f":{"0":7},"b":{"0":[2,5],"1":[1,4]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"bafc6316c9df86ff5f15cbcde7361ecae6a230c0"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/rateLimiters.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/middleware/rateLimiters.js","statementMap":{"0":{"start":{"line":1,"column":18},"end":{"line":1,"column":47}},"1":{"start":{"line":4,"column":13},"end":{"line":4,"column":50}},"2":{"start":{"line":4,"column":19},"end":{"line":4,"column":50}},"3":{"start":{"line":6,"column":19},"end":{"line":13,"column":2}},"4":{"start":{"line":15,"column":20},"end":{"line":22,"column":2}},"5":{"start":{"line":24,"column":18},"end":{"line":31,"column":2}},"6":{"start":{"line":33,"column":0},"end":{"line":33,"column":56}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":4,"column":13},"end":{"line":4,"column":14}},"loc":{"start":{"line":4,"column":19},"end":{"line":4,"column":50}},"line":4}},"branchMap":{},"s":{"0":6,"1":6,"2":223,"3":6,"4":6,"5":6,"6":6},"f":{"0":223},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"3f67edeba234e7eb1b887984ea4aca0ca4bd4ea2"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/models/Exercise.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/models/Exercise.js","statementMap":{"0":{"start":{"line":1,"column":17},"end":{"line":1,"column":36}},"1":{"start":{"line":3,"column":23},"end":{"line":10,"column":2}},"2":{"start":{"line":12,"column":0},"end":{"line":12,"column":60}}},"fnMap":{},"branchMap":{},"s":{"0":6,"1":6,"2":6},"f":{},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"9c84d48d925448b94d12a2108ed1b1805ce5de9f"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/models/Split.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/models/Split.js","statementMap":{"0":{"start":{"line":1,"column":17},"end":{"line":1,"column":36}},"1":{"start":{"line":3,"column":20},"end":{"line":23,"column":1}},"2":{"start":{"line":25,"column":0},"end":{"line":25,"column":54}}},"fnMap":{},"branchMap":{},"s":{"0":6,"1":6,"2":6},"f":{},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"9fa32a48c7013c75253e9b2b24b8e61cc675cf5b"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/models/User.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/models/User.js","statementMap":{"0":{"start":{"line":1,"column":17},"end":{"line":1,"column":36}},"1":{"start":{"line":3,"column":19},"end":{"line":51,"column":1}},"2":{"start":{"line":53,"column":0},"end":{"line":53,"column":52}}},"fnMap":{},"branchMap":{},"s":{"0":6,"1":6,"2":6},"f":{},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"53b3c5f18cdefe3ddd1efca799813f0365a1d04f"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/models/Workout.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/models/Workout.js","statementMap":{"0":{"start":{"line":1,"column":17},"end":{"line":1,"column":36}},"1":{"start":{"line":3,"column":22},"end":{"line":23,"column":1}},"2":{"start":{"line":25,"column":0},"end":{"line":25,"column":58}}},"fnMap":{},"branchMap":{},"s":{"0":6,"1":6,"2":6},"f":{},"b":{},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"c7cb1b034e1eabf1c6530a855f4ebad5364f5eff"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/ai.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/ai.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":17},"end":{"line":2,"column":46}},"2":{"start":{"line":3,"column":20},"end":{"line":3,"column":59}},"3":{"start":{"line":4,"column":16},"end":{"line":4,"column":44}},"4":{"start":{"line":5,"column":14},"end":{"line":5,"column":40}},"5":{"start":{"line":7,"column":15},"end":{"line":7,"column":31}},"6":{"start":{"line":9,"column":20},"end":{"line":18,"column":1}},"7":{"start":{"line":10,"column":21},"end":{"line":10,"column":38}},"8":{"start":{"line":11,"column":20},"end":{"line":11,"column":41}},"9":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"10":{"start":{"line":14,"column":4},"end":{"line":14,"column":57}},"11":{"start":{"line":17,"column":2},"end":{"line":17,"column":59}},"12":{"start":{"line":20,"column":0},"end":{"line":178,"column":3}},"13":{"start":{"line":21,"column":18},"end":{"line":21,"column":47}},"14":{"start":{"line":30,"column":6},"end":{"line":30,"column":13}},"15":{"start":{"line":33,"column":4},"end":{"line":39,"column":10}},"16":{"start":{"line":36,"column":41},"end":{"line":36,"column":62}},"17":{"start":{"line":41,"column":20},"end":{"line":43,"column":48}},"18":{"start":{"line":45,"column":2},"end":{"line":49,"column":3}},"19":{"start":{"line":46,"column":4},"end":{"line":48,"column":7}},"20":{"start":{"line":51,"column":26},"end":{"line":57,"column":5}},"21":{"start":{"line":51,"column":55},"end":{"line":57,"column":3}},"22":{"start":{"line":59,"column":23},"end":{"line":72,"column":1}},"23":{"start":{"line":74,"column":21},"end":{"line":112,"column":1}},"24":{"start":{"line":114,"column":22},"end":{"line":131,"column":4}},"25":{"start":{"line":133,"column":18},"end":{"line":133,"column":42}},"26":{"start":{"line":135,"column":2},"end":{"line":150,"column":3}},"27":{"start":{"line":136,"column":4},"end":{"line":136,"column":53}},"28":{"start":{"line":137,"column":4},"end":{"line":137,"column":74}},"29":{"start":{"line":138,"column":4},"end":{"line":138,"column":111}},"30":{"start":{"line":139,"column":4},"end":{"line":139,"column":107}},"31":{"start":{"line":140,"column":4},"end":{"line":140,"column":75}},"32":{"start":{"line":142,"column":4},"end":{"line":149,"column":7}},"33":{"start":{"line":152,"column":18},"end":{"line":152,"column":56}},"34":{"start":{"line":154,"column":2},"end":{"line":158,"column":3}},"35":{"start":{"line":155,"column":4},"end":{"line":157,"column":7}},"36":{"start":{"line":160,"column":15},"end":{"line":160,"column":35}},"37":{"start":{"line":162,"column":27},"end":{"line":162,"column":82}},"38":{"start":{"line":162,"column":69},"end":{"line":162,"column":80}},"39":{"start":{"line":164,"column":2},"end":{"line":173,"column":3}},"40":{"start":{"line":165,"column":4},"end":{"line":172,"column":5}},"41":{"start":{"line":166,"column":6},"end":{"line":171,"column":7}},"42":{"start":{"line":167,"column":8},"end":{"line":170,"column":11}},"43":{"start":{"line":175,"column":2},"end":{"line":177,"column":5}},"44":{"start":{"line":180,"column":0},"end":{"line":212,"column":3}},"45":{"start":{"line":181,"column":19},"end":{"line":181,"column":27}},"46":{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},"47":{"start":{"line":184,"column":4},"end":{"line":184,"column":71}},"48":{"start":{"line":187,"column":26},"end":{"line":187,"column":28}},"49":{"start":{"line":189,"column":2},"end":{"line":199,"column":3}},"50":{"start":{"line":190,"column":24},"end":{"line":190,"column":84}},"51":{"start":{"line":190,"column":64},"end":{"line":190,"column":83}},"52":{"start":{"line":192,"column":20},"end":{"line":196,"column":6}},"53":{"start":{"line":198,"column":4},"end":{"line":198,"column":34}},"54":{"start":{"line":201,"column":16},"end":{"line":205,"column":4}},"55":{"start":{"line":204,"column":47},"end":{"line":204,"column":58}},"56":{"start":{"line":207,"column":2},"end":{"line":211,"column":5}},"57":{"start":{"line":214,"column":0},"end":{"line":214,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":20},"end":{"line":9,"column":21}},"loc":{"start":{"line":9,"column":30},"end":{"line":18,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":20,"column":42},"end":{"line":20,"column":43}},"loc":{"start":{"line":20,"column":62},"end":{"line":178,"column":1}},"line":20},"2":{"name":"(anonymous_2)","decl":{"start":{"line":36,"column":31},"end":{"line":36,"column":32}},"loc":{"start":{"line":36,"column":41},"end":{"line":36,"column":62}},"line":36},"3":{"name":"(anonymous_3)","decl":{"start":{"line":51,"column":40},"end":{"line":51,"column":41}},"loc":{"start":{"line":51,"column":55},"end":{"line":57,"column":3}},"line":51},"4":{"name":"(anonymous_4)","decl":{"start":{"line":162,"column":55},"end":{"line":162,"column":56}},"loc":{"start":{"line":162,"column":69},"end":{"line":162,"column":80}},"line":162},"5":{"name":"(anonymous_5)","decl":{"start":{"line":180,"column":45},"end":{"line":180,"column":46}},"loc":{"start":{"line":180,"column":65},"end":{"line":212,"column":1}},"line":180},"6":{"name":"(anonymous_6)","decl":{"start":{"line":190,"column":50},"end":{"line":190,"column":51}},"loc":{"start":{"line":190,"column":64},"end":{"line":190,"column":83}},"line":190},"7":{"name":"(anonymous_7)","decl":{"start":{"line":204,"column":34},"end":{"line":204,"column":35}},"loc":{"start":{"line":204,"column":47},"end":{"line":204,"column":58}},"line":204}},"branchMap":{"0":{"loc":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"type":"if","locations":[{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},{"start":{},"end":{}}],"line":13},"1":{"loc":{"start":{"line":13,"column":6},"end":{"line":13,"column":43}},"type":"binary-expr","locations":[{"start":{"line":13,"column":6},"end":{"line":13,"column":23}},{"start":{"line":13,"column":27},"end":{"line":13,"column":43}}],"line":13},"2":{"loc":{"start":{"line":21,"column":18},"end":{"line":21,"column":47}},"type":"binary-expr","locations":[{"start":{"line":21,"column":18},"end":{"line":21,"column":41}},{"start":{"line":21,"column":45},"end":{"line":21,"column":47}}],"line":21},"3":{"loc":{"start":{"line":24,"column":4},"end":{"line":24,"column":28}},"type":"default-arg","locations":[{"start":{"line":24,"column":11},"end":{"line":24,"column":28}}],"line":24},"4":{"loc":{"start":{"line":25,"column":4},"end":{"line":25,"column":32}},"type":"default-arg","locations":[{"start":{"line":25,"column":22},"end":{"line":25,"column":32}}],"line":25},"5":{"loc":{"start":{"line":26,"column":4},"end":{"line":26,"column":19}},"type":"default-arg","locations":[{"start":{"line":26,"column":18},"end":{"line":26,"column":19}}],"line":26},"6":{"loc":{"start":{"line":27,"column":4},"end":{"line":27,"column":29}},"type":"default-arg","locations":[{"start":{"line":27,"column":27},"end":{"line":27,"column":29}}],"line":27},"7":{"loc":{"start":{"line":28,"column":4},"end":{"line":28,"column":18}},"type":"default-arg","locations":[{"start":{"line":28,"column":16},"end":{"line":28,"column":18}}],"line":28},"8":{"loc":{"start":{"line":29,"column":4},"end":{"line":29,"column":20}},"type":"default-arg","locations":[{"start":{"line":29,"column":18},"end":{"line":29,"column":20}}],"line":29},"9":{"loc":{"start":{"line":33,"column":4},"end":{"line":39,"column":10}},"type":"cond-expr","locations":[{"start":{"line":34,"column":8},"end":{"line":38,"column":9}},{"start":{"line":39,"column":8},"end":{"line":39,"column":10}}],"line":33},"10":{"loc":{"start":{"line":45,"column":2},"end":{"line":49,"column":3}},"type":"if","locations":[{"start":{"line":45,"column":2},"end":{"line":49,"column":3}},{"start":{},"end":{}}],"line":45},"11":{"loc":{"start":{"line":82,"column":15},"end":{"line":82,"column":54}},"type":"binary-expr","locations":[{"start":{"line":82,"column":15},"end":{"line":82,"column":35}},{"start":{"line":82,"column":39},"end":{"line":82,"column":54}}],"line":82},"12":{"loc":{"start":{"line":83,"column":17},"end":{"line":83,"column":49}},"type":"binary-expr","locations":[{"start":{"line":83,"column":17},"end":{"line":83,"column":39}},{"start":{"line":83,"column":43},"end":{"line":83,"column":49}}],"line":83},"13":{"loc":{"start":{"line":135,"column":2},"end":{"line":150,"column":3}},"type":"if","locations":[{"start":{"line":135,"column":2},"end":{"line":150,"column":3}},{"start":{},"end":{}}],"line":135},"14":{"loc":{"start":{"line":142,"column":22},"end":{"line":142,"column":60}},"type":"cond-expr","locations":[{"start":{"line":142,"column":51},"end":{"line":142,"column":54}},{"start":{"line":142,"column":57},"end":{"line":142,"column":60}}],"line":142},"15":{"loc":{"start":{"line":144,"column":8},"end":{"line":146,"column":32}},"type":"cond-expr","locations":[{"start":{"line":145,"column":12},"end":{"line":145,"column":56}},{"start":{"line":146,"column":12},"end":{"line":146,"column":32}}],"line":144},"16":{"loc":{"start":{"line":154,"column":2},"end":{"line":158,"column":3}},"type":"if","locations":[{"start":{"line":154,"column":2},"end":{"line":158,"column":3}},{"start":{},"end":{}}],"line":154},"17":{"loc":{"start":{"line":164,"column":20},"end":{"line":164,"column":35}},"type":"binary-expr","locations":[{"start":{"line":164,"column":20},"end":{"line":164,"column":29}},{"start":{"line":164,"column":33},"end":{"line":164,"column":35}}],"line":164},"18":{"loc":{"start":{"line":165,"column":27},"end":{"line":165,"column":46}},"type":"binary-expr","locations":[{"start":{"line":165,"column":27},"end":{"line":165,"column":40}},{"start":{"line":165,"column":44},"end":{"line":165,"column":46}}],"line":165},"19":{"loc":{"start":{"line":166,"column":6},"end":{"line":171,"column":7}},"type":"if","locations":[{"start":{"line":166,"column":6},"end":{"line":171,"column":7}},{"start":{},"end":{}}],"line":166},"20":{"loc":{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},"type":"if","locations":[{"start":{"line":183,"column":2},"end":{"line":185,"column":3}},{"start":{},"end":{}}],"line":183},"21":{"loc":{"start":{"line":183,"column":6},"end":{"line":183,"column":40}},"type":"binary-expr","locations":[{"start":{"line":183,"column":6},"end":{"line":183,"column":11}},{"start":{"line":183,"column":15},"end":{"line":183,"column":40}}],"line":183},"22":{"loc":{"start":{"line":190,"column":25},"end":{"line":190,"column":44}},"type":"binary-expr","locations":[{"start":{"line":190,"column":25},"end":{"line":190,"column":38}},{"start":{"line":190,"column":42},"end":{"line":190,"column":44}}],"line":190},"23":{"loc":{"start":{"line":194,"column":12},"end":{"line":194,"column":40}},"type":"binary-expr","locations":[{"start":{"line":194,"column":12},"end":{"line":194,"column":20}},{"start":{"line":194,"column":24},"end":{"line":194,"column":40}}],"line":194},"24":{"loc":{"start":{"line":203,"column":10},"end":{"line":203,"column":49}},"type":"binary-expr","locations":[{"start":{"line":203,"column":10},"end":{"line":203,"column":26}},{"start":{"line":203,"column":30},"end":{"line":203,"column":49}}],"line":203}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":4,"8":4,"9":4,"10":1,"11":3,"12":6,"13":8,"14":8,"15":8,"16":1,"17":8,"18":8,"19":1,"20":7,"21":7,"22":7,"23":7,"24":7,"25":6,"26":6,"27":1,"28":1,"29":1,"30":1,"31":1,"32":1,"33":5,"34":5,"35":1,"36":4,"37":3,"38":3,"39":3,"40":3,"41":3,"42":1,"43":2,"44":6,"45":2,"46":2,"47":1,"48":1,"49":1,"50":1,"51":1,"52":1,"53":1,"54":1,"55":1,"56":1,"57":6},"f":{"0":4,"1":8,"2":1,"3":7,"4":3,"5":2,"6":1,"7":1},"b":{"0":[1,3],"1":[4,3],"2":[8,0],"3":[0],"4":[0],"5":[0],"6":[0],"7":[0],"8":[0],"9":[1,7],"10":[1,7],"11":[7,6],"12":[7,7],"13":[1,5],"14":[1,0],"15":[1,0],"16":[1,4],"17":[3,0],"18":[3,0],"19":[1,2],"20":[1,1],"21":[2,2],"22":[1,0],"23":[1,0],"24":[1,0]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"960e258580e978209e861d0f84e7a6cbe9374cd2"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/auth.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/auth.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":15},"end":{"line":2,"column":34}},"2":{"start":{"line":3,"column":12},"end":{"line":3,"column":35}},"3":{"start":{"line":4,"column":13},"end":{"line":4,"column":38}},"4":{"start":{"line":5,"column":20},"end":{"line":5,"column":59}},"5":{"start":{"line":7,"column":15},"end":{"line":7,"column":31}},"6":{"start":{"line":9,"column":20},"end":{"line":13,"column":1}},"7":{"start":{"line":10,"column":4},"end":{"line":12,"column":7}},"8":{"start":{"line":15,"column":22},"end":{"line":20,"column":2}},"9":{"start":{"line":15,"column":33},"end":{"line":20,"column":1}},"10":{"start":{"line":22,"column":0},"end":{"line":52,"column":3}},"11":{"start":{"line":23,"column":42},"end":{"line":23,"column":50}},"12":{"start":{"line":25,"column":8},"end":{"line":27,"column":9}},"13":{"start":{"line":26,"column":12},"end":{"line":26,"column":95}},"14":{"start":{"line":29,"column":8},"end":{"line":31,"column":9}},"15":{"start":{"line":30,"column":12},"end":{"line":30,"column":95}},"16":{"start":{"line":33,"column":29},"end":{"line":33,"column":58}},"17":{"start":{"line":35,"column":8},"end":{"line":37,"column":9}},"18":{"start":{"line":36,"column":12},"end":{"line":36,"column":81}},"19":{"start":{"line":39,"column":29},"end":{"line":39,"column":60}},"20":{"start":{"line":41,"column":21},"end":{"line":45,"column":10}},"21":{"start":{"line":47,"column":8},"end":{"line":50,"column":11}},"22":{"start":{"line":54,"column":0},"end":{"line":77,"column":3}},"23":{"start":{"line":55,"column":36},"end":{"line":55,"column":44}},"24":{"start":{"line":57,"column":8},"end":{"line":59,"column":9}},"25":{"start":{"line":58,"column":12},"end":{"line":58,"column":88}},"26":{"start":{"line":61,"column":21},"end":{"line":61,"column":50}},"27":{"start":{"line":63,"column":8},"end":{"line":65,"column":9}},"28":{"start":{"line":64,"column":12},"end":{"line":64,"column":82}},"29":{"start":{"line":67,"column":32},"end":{"line":67,"column":81}},"30":{"start":{"line":69,"column":8},"end":{"line":71,"column":9}},"31":{"start":{"line":70,"column":12},"end":{"line":70,"column":82}},"32":{"start":{"line":73,"column":8},"end":{"line":76,"column":11}},"33":{"start":{"line":79,"column":0},"end":{"line":83,"column":3}},"34":{"start":{"line":80,"column":4},"end":{"line":82,"column":7}},"35":{"start":{"line":85,"column":0},"end":{"line":116,"column":3}},"36":{"start":{"line":94,"column":12},"end":{"line":94,"column":20}},"37":{"start":{"line":96,"column":21},"end":{"line":96,"column":54}},"38":{"start":{"line":98,"column":8},"end":{"line":100,"column":9}},"39":{"start":{"line":99,"column":12},"end":{"line":99,"column":71}},"40":{"start":{"line":102,"column":8},"end":{"line":110,"column":10}},"41":{"start":{"line":112,"column":8},"end":{"line":112,"column":26}},"42":{"start":{"line":114,"column":8},"end":{"line":114,"column":60}},"43":{"start":{"line":118,"column":0},"end":{"line":118,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":20},"end":{"line":9,"column":21}},"loc":{"start":{"line":9,"column":32},"end":{"line":13,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":15,"column":22},"end":{"line":15,"column":23}},"loc":{"start":{"line":15,"column":33},"end":{"line":20,"column":1}},"line":15},"2":{"name":"(anonymous_2)","decl":{"start":{"line":22,"column":25},"end":{"line":22,"column":26}},"loc":{"start":{"line":22,"column":45},"end":{"line":52,"column":1}},"line":22},"3":{"name":"(anonymous_3)","decl":{"start":{"line":54,"column":22},"end":{"line":54,"column":23}},"loc":{"start":{"line":54,"column":42},"end":{"line":77,"column":1}},"line":54},"4":{"name":"(anonymous_4)","decl":{"start":{"line":79,"column":27},"end":{"line":79,"column":28}},"loc":{"start":{"line":79,"column":47},"end":{"line":83,"column":1}},"line":79},"5":{"name":"(anonymous_5)","decl":{"start":{"line":85,"column":32},"end":{"line":85,"column":33}},"loc":{"start":{"line":85,"column":52},"end":{"line":116,"column":1}},"line":85}},"branchMap":{"0":{"loc":{"start":{"line":25,"column":8},"end":{"line":27,"column":9}},"type":"if","locations":[{"start":{"line":25,"column":8},"end":{"line":27,"column":9}},{"start":{},"end":{}}],"line":25},"1":{"loc":{"start":{"line":25,"column":12},"end":{"line":25,"column":40}},"type":"binary-expr","locations":[{"start":{"line":25,"column":12},"end":{"line":25,"column":17}},{"start":{"line":25,"column":21},"end":{"line":25,"column":27}},{"start":{"line":25,"column":31},"end":{"line":25,"column":40}}],"line":25},"2":{"loc":{"start":{"line":29,"column":8},"end":{"line":31,"column":9}},"type":"if","locations":[{"start":{"line":29,"column":8},"end":{"line":31,"column":9}},{"start":{},"end":{}}],"line":29},"3":{"loc":{"start":{"line":35,"column":8},"end":{"line":37,"column":9}},"type":"if","locations":[{"start":{"line":35,"column":8},"end":{"line":37,"column":9}},{"start":{},"end":{}}],"line":35},"4":{"loc":{"start":{"line":57,"column":8},"end":{"line":59,"column":9}},"type":"if","locations":[{"start":{"line":57,"column":8},"end":{"line":59,"column":9}},{"start":{},"end":{}}],"line":57},"5":{"loc":{"start":{"line":57,"column":12},"end":{"line":57,"column":31}},"type":"binary-expr","locations":[{"start":{"line":57,"column":12},"end":{"line":57,"column":18}},{"start":{"line":57,"column":22},"end":{"line":57,"column":31}}],"line":57},"6":{"loc":{"start":{"line":63,"column":8},"end":{"line":65,"column":9}},"type":"if","locations":[{"start":{"line":63,"column":8},"end":{"line":65,"column":9}},{"start":{},"end":{}}],"line":63},"7":{"loc":{"start":{"line":69,"column":8},"end":{"line":71,"column":9}},"type":"if","locations":[{"start":{"line":69,"column":8},"end":{"line":71,"column":9}},{"start":{},"end":{}}],"line":69},"8":{"loc":{"start":{"line":98,"column":8},"end":{"line":100,"column":9}},"type":"if","locations":[{"start":{"line":98,"column":8},"end":{"line":100,"column":9}},{"start":{},"end":{}}],"line":98}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":6,"7":56,"8":6,"9":59,"10":6,"11":58,"12":58,"13":1,"14":57,"15":1,"16":56,"17":56,"18":1,"19":55,"20":55,"21":55,"22":6,"23":4,"24":4,"25":1,"26":3,"27":3,"28":1,"29":2,"30":2,"31":1,"32":1,"33":6,"34":1,"35":6,"36":2,"37":2,"38":2,"39":0,"40":2,"41":2,"42":2,"43":6},"f":{"0":56,"1":59,"2":58,"3":4,"4":1,"5":2},"b":{"0":[1,57],"1":[58,57,57],"2":[1,56],"3":[1,55],"4":[1,3],"5":[4,4],"6":[1,2],"7":[1,1],"8":[0,2]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"bb8cb07f479a2ede43add7bff010827df682fb86"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/exercises.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/exercises.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":17},"end":{"line":2,"column":46}},"2":{"start":{"line":4,"column":15},"end":{"line":4,"column":31}},"3":{"start":{"line":7,"column":0},"end":{"line":31,"column":3}},"4":{"start":{"line":8,"column":44},"end":{"line":8,"column":53}},"5":{"start":{"line":10,"column":14},"end":{"line":10,"column":16}},"6":{"start":{"line":11,"column":2},"end":{"line":20,"column":3}},"7":{"start":{"line":12,"column":4},"end":{"line":19,"column":6}},"8":{"start":{"line":22,"column":22},"end":{"line":29,"column":6}},"9":{"start":{"line":30,"column":4},"end":{"line":30,"column":36}},"10":{"start":{"line":33,"column":0},"end":{"line":33,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":7,"column":16},"end":{"line":7,"column":17}},"loc":{"start":{"line":7,"column":36},"end":{"line":31,"column":1}},"line":7}},"branchMap":{"0":{"loc":{"start":{"line":8,"column":18},"end":{"line":8,"column":26}},"type":"default-arg","locations":[{"start":{"line":8,"column":25},"end":{"line":8,"column":26}}],"line":8},"1":{"loc":{"start":{"line":8,"column":28},"end":{"line":8,"column":39}},"type":"default-arg","locations":[{"start":{"line":8,"column":36},"end":{"line":8,"column":39}}],"line":8},"2":{"loc":{"start":{"line":11,"column":2},"end":{"line":20,"column":3}},"type":"if","locations":[{"start":{"line":11,"column":2},"end":{"line":20,"column":3}},{"start":{},"end":{}}],"line":11}},"s":{"0":6,"1":6,"2":6,"3":6,"4":3,"5":3,"6":3,"7":1,"8":3,"9":3,"10":6},"f":{"0":3},"b":{"0":[2],"1":[2],"2":[1,2]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"c11eca912e5fdd851e2d51b3878275f9076d28da"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/splits.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/splits.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":14},"end":{"line":2,"column":40}},"2":{"start":{"line":3,"column":17},"end":{"line":3,"column":46}},"3":{"start":{"line":4,"column":20},"end":{"line":4,"column":59}},"4":{"start":{"line":6,"column":15},"end":{"line":6,"column":31}},"5":{"start":{"line":9,"column":0},"end":{"line":31,"column":3}},"6":{"start":{"line":10,"column":29},"end":{"line":10,"column":37}},"7":{"start":{"line":12,"column":2},"end":{"line":14,"column":3}},"8":{"start":{"line":13,"column":4},"end":{"line":13,"column":71}},"9":{"start":{"line":16,"column":2},"end":{"line":18,"column":3}},"10":{"start":{"line":17,"column":4},"end":{"line":17,"column":74}},"11":{"start":{"line":20,"column":21},"end":{"line":24,"column":6}},"12":{"start":{"line":26,"column":4},"end":{"line":26,"column":26}},"13":{"start":{"line":28,"column":27},"end":{"line":28,"column":82}},"14":{"start":{"line":30,"column":4},"end":{"line":30,"column":41}},"15":{"start":{"line":34,"column":0},"end":{"line":38,"column":3}},"16":{"start":{"line":35,"column":19},"end":{"line":35,"column":82}},"17":{"start":{"line":36,"column":4},"end":{"line":36,"column":33}},"18":{"start":{"line":41,"column":0},"end":{"line":64,"column":3}},"19":{"start":{"line":42,"column":18},"end":{"line":45,"column":27}},"20":{"start":{"line":47,"column":4},"end":{"line":49,"column":5}},"21":{"start":{"line":48,"column":6},"end":{"line":48,"column":66}},"22":{"start":{"line":51,"column":21},"end":{"line":51,"column":37}},"23":{"start":{"line":52,"column":24},"end":{"line":52,"column":75}},"24":{"start":{"line":52,"column":57},"end":{"line":52,"column":74}},"25":{"start":{"line":54,"column":4},"end":{"line":61,"column":5}},"26":{"start":{"line":55,"column":24},"end":{"line":55,"column":73}},"27":{"start":{"line":56,"column":19},"end":{"line":56,"column":59}},"28":{"start":{"line":56,"column":48},"end":{"line":56,"column":57}},"29":{"start":{"line":57,"column":6},"end":{"line":60,"column":10}},"30":{"start":{"line":57,"column":56},"end":{"line":60,"column":7}},"31":{"start":{"line":59,"column":51},"end":{"line":59,"column":63}},"32":{"start":{"line":63,"column":4},"end":{"line":63,"column":35}},"33":{"start":{"line":67,"column":0},"end":{"line":78,"column":3}},"34":{"start":{"line":68,"column":18},"end":{"line":71,"column":27}},"35":{"start":{"line":73,"column":4},"end":{"line":75,"column":5}},"36":{"start":{"line":74,"column":6},"end":{"line":74,"column":66}},"37":{"start":{"line":77,"column":4},"end":{"line":77,"column":32}},"38":{"start":{"line":81,"column":0},"end":{"line":113,"column":3}},"39":{"start":{"line":82,"column":29},"end":{"line":82,"column":37}},"40":{"start":{"line":84,"column":2},"end":{"line":86,"column":3}},"41":{"start":{"line":85,"column":4},"end":{"line":85,"column":71}},"42":{"start":{"line":88,"column":2},"end":{"line":90,"column":3}},"43":{"start":{"line":89,"column":4},"end":{"line":89,"column":74}},"44":{"start":{"line":92,"column":18},"end":{"line":105,"column":26}},"45":{"start":{"line":107,"column":4},"end":{"line":109,"column":5}},"46":{"start":{"line":108,"column":6},"end":{"line":108,"column":66}},"47":{"start":{"line":111,"column":4},"end":{"line":111,"column":32}},"48":{"start":{"line":116,"column":0},"end":{"line":127,"column":3}},"49":{"start":{"line":117,"column":18},"end":{"line":120,"column":6}},"50":{"start":{"line":122,"column":4},"end":{"line":124,"column":5}},"51":{"start":{"line":123,"column":6},"end":{"line":123,"column":66}},"52":{"start":{"line":126,"column":4},"end":{"line":126,"column":68}},"53":{"start":{"line":129,"column":0},"end":{"line":129,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":26},"end":{"line":9,"column":27}},"loc":{"start":{"line":9,"column":46},"end":{"line":31,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":34,"column":25},"end":{"line":34,"column":26}},"loc":{"start":{"line":34,"column":45},"end":{"line":38,"column":1}},"line":34},"2":{"name":"(anonymous_2)","decl":{"start":{"line":41,"column":36},"end":{"line":41,"column":37}},"loc":{"start":{"line":41,"column":56},"end":{"line":64,"column":1}},"line":41},"3":{"name":"(anonymous_3)","decl":{"start":{"line":52,"column":50},"end":{"line":52,"column":51}},"loc":{"start":{"line":52,"column":57},"end":{"line":52,"column":74}},"line":52},"4":{"name":"(anonymous_4)","decl":{"start":{"line":56,"column":41},"end":{"line":56,"column":42}},"loc":{"start":{"line":56,"column":48},"end":{"line":56,"column":57}},"line":56},"5":{"name":"(anonymous_5)","decl":{"start":{"line":57,"column":48},"end":{"line":57,"column":49}},"loc":{"start":{"line":57,"column":56},"end":{"line":60,"column":7}},"line":57},"6":{"name":"(anonymous_6)","decl":{"start":{"line":59,"column":43},"end":{"line":59,"column":44}},"loc":{"start":{"line":59,"column":51},"end":{"line":59,"column":63}},"line":59},"7":{"name":"(anonymous_7)","decl":{"start":{"line":67,"column":28},"end":{"line":67,"column":29}},"loc":{"start":{"line":67,"column":48},"end":{"line":78,"column":1}},"line":67},"8":{"name":"(anonymous_8)","decl":{"start":{"line":81,"column":28},"end":{"line":81,"column":29}},"loc":{"start":{"line":81,"column":48},"end":{"line":113,"column":1}},"line":81},"9":{"name":"(anonymous_9)","decl":{"start":{"line":116,"column":31},"end":{"line":116,"column":32}},"loc":{"start":{"line":116,"column":51},"end":{"line":127,"column":1}},"line":116}},"branchMap":{"0":{"loc":{"start":{"line":12,"column":2},"end":{"line":14,"column":3}},"type":"if","locations":[{"start":{"line":12,"column":2},"end":{"line":14,"column":3}},{"start":{},"end":{}}],"line":12},"1":{"loc":{"start":{"line":12,"column":6},"end":{"line":12,"column":27}},"type":"binary-expr","locations":[{"start":{"line":12,"column":6},"end":{"line":12,"column":11}},{"start":{"line":12,"column":15},"end":{"line":12,"column":27}}],"line":12},"2":{"loc":{"start":{"line":16,"column":2},"end":{"line":18,"column":3}},"type":"if","locations":[{"start":{"line":16,"column":2},"end":{"line":18,"column":3}},{"start":{},"end":{}}],"line":16},"3":{"loc":{"start":{"line":47,"column":4},"end":{"line":49,"column":5}},"type":"if","locations":[{"start":{"line":47,"column":4},"end":{"line":49,"column":5}},{"start":{},"end":{}}],"line":47},"4":{"loc":{"start":{"line":52,"column":57},"end":{"line":52,"column":74}},"type":"binary-expr","locations":[{"start":{"line":52,"column":57},"end":{"line":52,"column":68}},{"start":{"line":52,"column":72},"end":{"line":52,"column":74}}],"line":52},"5":{"loc":{"start":{"line":54,"column":4},"end":{"line":61,"column":5}},"type":"if","locations":[{"start":{"line":54,"column":4},"end":{"line":61,"column":5}},{"start":{},"end":{}}],"line":54},"6":{"loc":{"start":{"line":59,"column":20},"end":{"line":59,"column":37}},"type":"binary-expr","locations":[{"start":{"line":59,"column":20},"end":{"line":59,"column":31}},{"start":{"line":59,"column":35},"end":{"line":59,"column":37}}],"line":59},"7":{"loc":{"start":{"line":73,"column":4},"end":{"line":75,"column":5}},"type":"if","locations":[{"start":{"line":73,"column":4},"end":{"line":75,"column":5}},{"start":{},"end":{}}],"line":73},"8":{"loc":{"start":{"line":84,"column":2},"end":{"line":86,"column":3}},"type":"if","locations":[{"start":{"line":84,"column":2},"end":{"line":86,"column":3}},{"start":{},"end":{}}],"line":84},"9":{"loc":{"start":{"line":84,"column":6},"end":{"line":84,"column":27}},"type":"binary-expr","locations":[{"start":{"line":84,"column":6},"end":{"line":84,"column":11}},{"start":{"line":84,"column":15},"end":{"line":84,"column":27}}],"line":84},"10":{"loc":{"start":{"line":88,"column":2},"end":{"line":90,"column":3}},"type":"if","locations":[{"start":{"line":88,"column":2},"end":{"line":90,"column":3}},{"start":{},"end":{}}],"line":88},"11":{"loc":{"start":{"line":107,"column":4},"end":{"line":109,"column":5}},"type":"if","locations":[{"start":{"line":107,"column":4},"end":{"line":109,"column":5}},{"start":{},"end":{}}],"line":107},"12":{"loc":{"start":{"line":122,"column":4},"end":{"line":124,"column":5}},"type":"if","locations":[{"start":{"line":122,"column":4},"end":{"line":124,"column":5}},{"start":{},"end":{}}],"line":122}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":12,"7":12,"8":1,"9":11,"10":1,"11":10,"12":10,"13":10,"14":10,"15":6,"16":1,"17":1,"18":6,"19":2,"20":2,"21":1,"22":1,"23":1,"24":1,"25":1,"26":1,"27":1,"28":1,"29":1,"30":1,"31":1,"32":1,"33":6,"34":3,"35":3,"36":2,"37":1,"38":6,"39":4,"40":4,"41":1,"42":3,"43":1,"44":2,"45":2,"46":1,"47":1,"48":6,"49":2,"50":2,"51":1,"52":1,"53":6},"f":{"0":12,"1":1,"2":2,"3":1,"4":1,"5":1,"6":1,"7":3,"8":4,"9":2},"b":{"0":[1,11],"1":[12,11],"2":[1,10],"3":[1,1],"4":[1,0],"5":[1,0],"6":[1,0],"7":[2,1],"8":[1,3],"9":[4,3],"10":[1,2],"11":[1,1],"12":[1,1]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"3f9beca440eb5cba6eac9c0fdba1c15b2985f866"} -,"/mnt/c/users/Nathan Huang/FitForge/backend/routes/workouts.js": {"path":"/mnt/c/users/Nathan Huang/FitForge/backend/routes/workouts.js","statementMap":{"0":{"start":{"line":1,"column":16},"end":{"line":1,"column":34}},"1":{"start":{"line":2,"column":17},"end":{"line":2,"column":46}},"2":{"start":{"line":3,"column":16},"end":{"line":3,"column":44}},"3":{"start":{"line":4,"column":20},"end":{"line":4,"column":59}},"4":{"start":{"line":6,"column":15},"end":{"line":6,"column":31}},"5":{"start":{"line":9,"column":0},"end":{"line":30,"column":3}},"6":{"start":{"line":10,"column":30},"end":{"line":10,"column":38}},"7":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"8":{"start":{"line":14,"column":4},"end":{"line":14,"column":73}},"9":{"start":{"line":17,"column":2},"end":{"line":19,"column":3}},"10":{"start":{"line":18,"column":4},"end":{"line":18,"column":84}},"11":{"start":{"line":22,"column":20},"end":{"line":26,"column":6}},"12":{"start":{"line":27,"column":4},"end":{"line":27,"column":25}},"13":{"start":{"line":28,"column":4},"end":{"line":28,"column":34}},"14":{"start":{"line":33,"column":0},"end":{"line":50,"column":3}},"15":{"start":{"line":34,"column":21},"end":{"line":34,"column":65}},"16":{"start":{"line":37,"column":30},"end":{"line":47,"column":5}},"17":{"start":{"line":39,"column":26},"end":{"line":41,"column":10}},"18":{"start":{"line":42,"column":8},"end":{"line":45,"column":10}},"19":{"start":{"line":49,"column":4},"end":{"line":49,"column":44}},"20":{"start":{"line":53,"column":0},"end":{"line":64,"column":3}},"21":{"start":{"line":55,"column":20},"end":{"line":58,"column":6}},"22":{"start":{"line":60,"column":4},"end":{"line":62,"column":5}},"23":{"start":{"line":61,"column":6},"end":{"line":61,"column":68}},"24":{"start":{"line":63,"column":4},"end":{"line":63,"column":34}},"25":{"start":{"line":67,"column":0},"end":{"line":99,"column":3}},"26":{"start":{"line":68,"column":30},"end":{"line":68,"column":38}},"27":{"start":{"line":70,"column":2},"end":{"line":72,"column":3}},"28":{"start":{"line":71,"column":4},"end":{"line":71,"column":73}},"29":{"start":{"line":74,"column":2},"end":{"line":76,"column":3}},"30":{"start":{"line":75,"column":4},"end":{"line":75,"column":84}},"31":{"start":{"line":79,"column":20},"end":{"line":92,"column":5}},"32":{"start":{"line":94,"column":4},"end":{"line":96,"column":5}},"33":{"start":{"line":95,"column":6},"end":{"line":95,"column":68}},"34":{"start":{"line":98,"column":4},"end":{"line":98,"column":34}},"35":{"start":{"line":102,"column":0},"end":{"line":115,"column":3}},"36":{"start":{"line":104,"column":20},"end":{"line":107,"column":6}},"37":{"start":{"line":109,"column":4},"end":{"line":111,"column":5}},"38":{"start":{"line":110,"column":6},"end":{"line":110,"column":68}},"39":{"start":{"line":113,"column":4},"end":{"line":113,"column":70}},"40":{"start":{"line":117,"column":0},"end":{"line":117,"column":24}}},"fnMap":{"0":{"name":"(anonymous_0)","decl":{"start":{"line":9,"column":26},"end":{"line":9,"column":27}},"loc":{"start":{"line":9,"column":46},"end":{"line":30,"column":1}},"line":9},"1":{"name":"(anonymous_1)","decl":{"start":{"line":33,"column":25},"end":{"line":33,"column":26}},"loc":{"start":{"line":33,"column":45},"end":{"line":50,"column":1}},"line":33},"2":{"name":"(anonymous_2)","decl":{"start":{"line":38,"column":19},"end":{"line":38,"column":20}},"loc":{"start":{"line":38,"column":38},"end":{"line":46,"column":7}},"line":38},"3":{"name":"(anonymous_3)","decl":{"start":{"line":53,"column":28},"end":{"line":53,"column":29}},"loc":{"start":{"line":53,"column":48},"end":{"line":64,"column":1}},"line":53},"4":{"name":"(anonymous_4)","decl":{"start":{"line":67,"column":28},"end":{"line":67,"column":29}},"loc":{"start":{"line":67,"column":48},"end":{"line":99,"column":1}},"line":67},"5":{"name":"(anonymous_5)","decl":{"start":{"line":102,"column":31},"end":{"line":102,"column":32}},"loc":{"start":{"line":102,"column":51},"end":{"line":115,"column":1}},"line":102}},"branchMap":{"0":{"loc":{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},"type":"if","locations":[{"start":{"line":13,"column":2},"end":{"line":15,"column":3}},{"start":{},"end":{}}],"line":13},"1":{"loc":{"start":{"line":13,"column":6},"end":{"line":13,"column":27}},"type":"binary-expr","locations":[{"start":{"line":13,"column":6},"end":{"line":13,"column":11}},{"start":{"line":13,"column":15},"end":{"line":13,"column":27}}],"line":13},"2":{"loc":{"start":{"line":17,"column":2},"end":{"line":19,"column":3}},"type":"if","locations":[{"start":{"line":17,"column":2},"end":{"line":19,"column":3}},{"start":{},"end":{}}],"line":17},"3":{"loc":{"start":{"line":17,"column":6},"end":{"line":17,"column":57}},"type":"binary-expr","locations":[{"start":{"line":17,"column":6},"end":{"line":17,"column":31}},{"start":{"line":17,"column":35},"end":{"line":17,"column":57}}],"line":17},"4":{"loc":{"start":{"line":60,"column":4},"end":{"line":62,"column":5}},"type":"if","locations":[{"start":{"line":60,"column":4},"end":{"line":62,"column":5}},{"start":{},"end":{}}],"line":60},"5":{"loc":{"start":{"line":70,"column":2},"end":{"line":72,"column":3}},"type":"if","locations":[{"start":{"line":70,"column":2},"end":{"line":72,"column":3}},{"start":{},"end":{}}],"line":70},"6":{"loc":{"start":{"line":70,"column":6},"end":{"line":70,"column":27}},"type":"binary-expr","locations":[{"start":{"line":70,"column":6},"end":{"line":70,"column":11}},{"start":{"line":70,"column":15},"end":{"line":70,"column":27}}],"line":70},"7":{"loc":{"start":{"line":74,"column":2},"end":{"line":76,"column":3}},"type":"if","locations":[{"start":{"line":74,"column":2},"end":{"line":76,"column":3}},{"start":{},"end":{}}],"line":74},"8":{"loc":{"start":{"line":74,"column":6},"end":{"line":74,"column":57}},"type":"binary-expr","locations":[{"start":{"line":74,"column":6},"end":{"line":74,"column":31}},{"start":{"line":74,"column":35},"end":{"line":74,"column":57}}],"line":74},"9":{"loc":{"start":{"line":94,"column":4},"end":{"line":96,"column":5}},"type":"if","locations":[{"start":{"line":94,"column":4},"end":{"line":96,"column":5}},{"start":{},"end":{}}],"line":94},"10":{"loc":{"start":{"line":109,"column":4},"end":{"line":111,"column":5}},"type":"if","locations":[{"start":{"line":109,"column":4},"end":{"line":111,"column":5}},{"start":{},"end":{}}],"line":109}},"s":{"0":6,"1":6,"2":6,"3":6,"4":6,"5":6,"6":15,"7":15,"8":1,"9":14,"10":2,"11":12,"12":12,"13":12,"14":6,"15":1,"16":1,"17":1,"18":1,"19":1,"20":6,"21":6,"22":5,"23":3,"24":2,"25":6,"26":4,"27":4,"28":1,"29":3,"30":1,"31":2,"32":2,"33":1,"34":1,"35":6,"36":2,"37":2,"38":1,"39":1,"40":6},"f":{"0":15,"1":1,"2":1,"3":6,"4":4,"5":2},"b":{"0":[1,14],"1":[15,14],"2":[2,12],"3":[14,13],"4":[3,2],"5":[1,3],"6":[4,3],"7":[1,2],"8":[3,3],"9":[1,1],"10":[1,1]},"_coverageSchema":"1a1c01bbd47fc00a2c39e90264f33305004495a9","hash":"4e01a578f1f069b10bd102e6b3ed8954e91e8781"} -} diff --git a/backend/coverage/lcov-report/backend/app.js.html b/backend/coverage/lcov-report/backend/app.js.html deleted file mode 100644 index 036a6bf..0000000 --- a/backend/coverage/lcov-report/backend/app.js.html +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - Code coverage report for backend/app.js - - - - - - - - - -
-
-

All files / backend app.js

-
- -
- 100% - Statements - 33/33 -
- - -
- 100% - Branches - 6/6 -
- - -
- 100% - Functions - 3/3 -
- - -
- 100% - Lines - 32/32 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -496x -6x -6x -6x -6x -6x -  -6x -6x -6x -6x -6x -6x -  -6x -  -6x -  -6x -  -  -6x -6x -  -  -143x -1x -  -  -  -6x -  -6x -6x -6x -6x -6x -6x -  -  -6x -6x -1x -  -  -6x -6x -  -6x
const express = require("express");
-const cors = require("cors");
-const helmet = require("helmet");
-const bodyParser = require("body-parser");
-const path = require("path");
-require("express-async-errors");
- 
-const exerciseRoutes = require("./routes/exercises");
-const workoutRoutes = require("./routes/workouts");
-const splitRoutes = require("./routes/splits");
-const authRoutes = require("./routes/auth");
-const aiRoutes = require("./routes/ai");
-const { apiLimiter, authLimiter, aiLimiter } = require("./middleware/rateLimiters");
- 
-const app = express();
- 
-const allowedOrigins = (process.env.CORS_ORIGIN || "http://localhost:3000")
-  .split(",")
-  .map((origin) => origin.trim())
-  .filter(Boolean);
- 
-app.use(helmet({ contentSecurityPolicy: false }));
-app.use(
-  cors({
-    origin: (origin, callback) => {
-      if (!origin || allowedOrigins.includes(origin)) return callback(null, true);
-      return callback(new Error("Not allowed by CORS"));
-    },
-  })
-);
-app.use(bodyParser.json({ limit: "1mb" }));
- 
-app.use("/api", apiLimiter);
-app.use("/api/exercises", exerciseRoutes);
-app.use("/api/workouts", workoutRoutes);
-app.use("/api/splits", splitRoutes);
-app.use("/api/auth", authLimiter, authRoutes);
-app.use("/api/ai", aiLimiter, aiRoutes);
- 
-// Serve the React build + SPA catchall
-app.use(express.static(path.join(__dirname, "../frontend/build")));
-app.get("*", (req, res) => {
-  res.sendFile(path.join(__dirname, "../frontend/build", "index.html"));
-});
- 
-const errorHandler = require("./middleware/errorHandler");
-app.use(errorHandler);
- 
-module.exports = app;
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/index.html b/backend/coverage/lcov-report/backend/index.html deleted file mode 100644 index d506997..0000000 --- a/backend/coverage/lcov-report/backend/index.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - Code coverage report for backend - - - - - - - - - -
-
-

All files backend

-
- -
- 100% - Statements - 33/33 -
- - -
- 100% - Branches - 6/6 -
- - -
- 100% - Functions - 3/3 -
- - -
- 100% - Lines - 32/32 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
app.js -
-
100%33/33100%6/6100%3/3100%32/32
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/middleware/authMiddleware.js.html b/backend/coverage/lcov-report/backend/middleware/authMiddleware.js.html deleted file mode 100644 index 8298956..0000000 --- a/backend/coverage/lcov-report/backend/middleware/authMiddleware.js.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - Code coverage report for backend/middleware/authMiddleware.js - - - - - - - - - -
-
-

All files / backend/middleware authMiddleware.js

-
- -
- 100% - Statements - 16/16 -
- - -
- 100% - Branches - 6/6 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 16/16 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -236x -6x -  -6x -76x -76x -76x -8x -  -68x -68x -66x -66x -1x -  -65x -65x -  -2x -  -  -  -6x
const jwt = require("jsonwebtoken");
-const User = require("../models/User");
- 
-const protect = async (req, res, next) => {
-  try {
-    const authHeader = req.headers.authorization;
-    if (!authHeader || !authHeader.startsWith("Bearer ")) {
-      return res.status(401).json({ message: "Not authorized, no token" });
-    }
-    const token = authHeader.split(" ")[1];
-    const decoded = jwt.verify(token, process.env.JWT_SECRET);
-    const user = await User.findById(decoded.userId).select("-passwordHash");
-    if (!user) {
-      return res.status(401).json({ message: "Not authorized, user not found" });
-    }
-    req.user = user;
-    next();
-  } catch (error) {
-    return res.status(401).json({ message: "Not authorized, token failed" });
-  }
-};
- 
-module.exports = { protect };
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/middleware/errorHandler.js.html b/backend/coverage/lcov-report/backend/middleware/errorHandler.js.html deleted file mode 100644 index e80dbd3..0000000 --- a/backend/coverage/lcov-report/backend/middleware/errorHandler.js.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - Code coverage report for backend/middleware/errorHandler.js - - - - - - - - - -
-
-

All files / backend/middleware errorHandler.js

-
- -
- 100% - Statements - 8/8 -
- - -
- 100% - Branches - 4/4 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 8/8 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13  -7x -7x -2x -  -5x -1x -  -4x -4x -  -  -7x
// Central error handler — must be the LAST app.use().
-const errorHandler = (err, req, res, next) => {
-  if (err.name === "CastError") {
-    return res.status(400).json({ message: "Invalid ID format" });   // bad ObjectId
-  }
-  if (err.name === "ValidationError") {
-    return res.status(400).json({ message: err.message });           // mongoose validation
-  }
-  console.error("Unhandled error:", err.message);
-  res.status(500).json({ message: "Server Error" });
-};
- 
-module.exports = errorHandler;
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/middleware/index.html b/backend/coverage/lcov-report/backend/middleware/index.html deleted file mode 100644 index e89c315..0000000 --- a/backend/coverage/lcov-report/backend/middleware/index.html +++ /dev/null @@ -1,146 +0,0 @@ - - - - - - Code coverage report for backend/middleware - - - - - - - - - -
-
-

All files backend/middleware

-
- -
- 100% - Statements - 31/31 -
- - -
- 100% - Branches - 10/10 -
- - -
- 100% - Functions - 3/3 -
- - -
- 100% - Lines - 30/30 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
authMiddleware.js -
-
100%16/16100%6/6100%1/1100%16/16
errorHandler.js -
-
100%8/8100%4/4100%1/1100%8/8
rateLimiters.js -
-
100%7/7100%0/0100%1/1100%6/6
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/middleware/rateLimiters.js.html b/backend/coverage/lcov-report/backend/middleware/rateLimiters.js.html deleted file mode 100644 index c9906f5..0000000 --- a/backend/coverage/lcov-report/backend/middleware/rateLimiters.js.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - Code coverage report for backend/middleware/rateLimiters.js - - - - - - - - - -
-
-

All files / backend/middleware rateLimiters.js

-
- -
- 100% - Statements - 7/7 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 6/6 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -336x -  -  -223x -  -6x -  -  -  -  -  -  -  -  -6x -  -  -  -  -  -  -  -  -6x -  -  -  -  -  -  -  -  -6x
const rateLimit = require("express-rate-limit");
- 
-// Disable rate limiting during tests so the suite isn't throttled.
-const skip = () => process.env.NODE_ENV === "test";
- 
-const apiLimiter = rateLimit({
-  windowMs: 15 * 60 * 1000,
-  max: 300,
-  standardHeaders: true,
-  legacyHeaders: false,
-  skip,
-  message: { message: "Too many requests, please try again later." },
-});
- 
-const authLimiter = rateLimit({
-  windowMs: 15 * 60 * 1000,
-  max: 20,
-  standardHeaders: true,
-  legacyHeaders: false,
-  skip,
-  message: { message: "Too many authentication attempts, please try again later." },
-});
- 
-const aiLimiter = rateLimit({
-  windowMs: 60 * 60 * 1000,
-  max: 30,
-  standardHeaders: true,
-  legacyHeaders: false,
-  skip,
-  message: { message: "AI generation limit reached, please try again later." },
-});
- 
-module.exports = { apiLimiter, authLimiter, aiLimiter };
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/Exercise.js.html b/backend/coverage/lcov-report/backend/models/Exercise.js.html deleted file mode 100644 index 854d40f..0000000 --- a/backend/coverage/lcov-report/backend/models/Exercise.js.html +++ /dev/null @@ -1,121 +0,0 @@ - - - - - - Code coverage report for backend/models/Exercise.js - - - - - - - - - -
-
-

All files / backend/models Exercise.js

-
- -
- 100% - Statements - 3/3 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 3/3 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -136x -  -6x -  -  -  -  -  -  -  -  -6x - 
const mongoose = require("mongoose");
- 
-const exerciseSchema = new mongoose.Schema({
-  id: Number,
-  name: String,
-  bodyPart: String,
-  equipment: String,
-  target: String,
-  gifUrl: String,
-});
- 
-module.exports = mongoose.model("Exercise", exerciseSchema);
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/Split.js.html b/backend/coverage/lcov-report/backend/models/Split.js.html deleted file mode 100644 index 493158f..0000000 --- a/backend/coverage/lcov-report/backend/models/Split.js.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - Code coverage report for backend/models/Split.js - - - - - - - - - -
-
-

All files / backend/models Split.js

-
- -
- 100% - Statements - 3/3 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 3/3 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -266x -  -6x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -6x - 
const mongoose = require("mongoose");
- 
-const splitSchema = new mongoose.Schema({
-  userId: {
-    type: mongoose.Schema.Types.ObjectId,
-    ref: "User",
-    required: true,
-    index: true,
-  },
-  name: {
-    type: String,
-    required: true,
-    trim: true,
-  },
-  workouts: [
-    {
-      type: mongoose.Schema.Types.ObjectId,
-      ref: "Workout",
-    }
-  ],
-},
-  { timestamps: true }
-);
- 
-module.exports = mongoose.model("Split", splitSchema);
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/User.js.html b/backend/coverage/lcov-report/backend/models/User.js.html deleted file mode 100644 index cb59ad6..0000000 --- a/backend/coverage/lcov-report/backend/models/User.js.html +++ /dev/null @@ -1,241 +0,0 @@ - - - - - - Code coverage report for backend/models/User.js - - - - - - - - - -
-
-

All files / backend/models User.js

-
- -
- 100% - Statements - 3/3 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 3/3 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -536x -  -6x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -6x
const mongoose = require("mongoose");
- 
-const userSchema = new mongoose.Schema(
-    {
-        name: {
-            type: String,
-            required: true,
-            trim: true
-        },
-        email: {
-            type: String,
-            required: true,
-            unique: true,
-            lowercase: true,
-            trim: true,
-        },
-        passwordHash: {
-            type: String,
-            required: true,
-        },
-        fitnessProfile: {
-            goal: {
-                type: String,
-                enum: ["strength", "hypertrophy", "fat_loss", "general_fitness"],
-                default: "general_fitness",
-            },
-            experienceLevel: {
-                type: String,
-                enum: ["beginner", "intermediate", "advanced"],
-                default: "beginner",
-            },
-            daysPerWeek: {
-                type: Number,
-                default: 3,
-            },
-            sessionLengthMinutes: {
-                type: Number,
-                default: 60,
-            },
-            equipment: {
-                type: [String],
-                default: [],
-            },
-            limitations: {
-                type: [String],
-                default: [],
-            },
-        },
-    },
-    {timestamps: true}
-);
- 
-module.exports = mongoose.model("User", userSchema);
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/Workout.js.html b/backend/coverage/lcov-report/backend/models/Workout.js.html deleted file mode 100644 index 97f5c8a..0000000 --- a/backend/coverage/lcov-report/backend/models/Workout.js.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - Code coverage report for backend/models/Workout.js - - - - - - - - - -
-
-

All files / backend/models Workout.js

-
- -
- 100% - Statements - 3/3 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 3/3 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -266x -  -6x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -6x - 
const mongoose = require("mongoose");
- 
-const workoutSchema = new mongoose.Schema(
-  {
-    userId: {
-      type: mongoose.Schema.Types.ObjectId,
-      ref: "User",
-      required: true,
-      index: true,
-    },
-    name: {
-      type: String,
-      required: true,
-      trim: true,
-    },
-    exercises: [
-      {
-        type: Number, 
-      },
-    ],
-  },
-  { timestamps: true }
-);
- 
-module.exports = mongoose.model("Workout", workoutSchema);
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/models/index.html b/backend/coverage/lcov-report/backend/models/index.html deleted file mode 100644 index 9c1594d..0000000 --- a/backend/coverage/lcov-report/backend/models/index.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - Code coverage report for backend/models - - - - - - - - - -
-
-

All files backend/models

-
- -
- 100% - Statements - 12/12 -
- - -
- 100% - Branches - 0/0 -
- - -
- 100% - Functions - 0/0 -
- - -
- 100% - Lines - 12/12 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
Exercise.js -
-
100%3/3100%0/0100%0/0100%3/3
Split.js -
-
100%3/3100%0/0100%0/0100%3/3
User.js -
-
100%3/3100%0/0100%0/0100%3/3
Workout.js -
-
100%3/3100%0/0100%0/0100%3/3
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/ai.js.html b/backend/coverage/lcov-report/backend/routes/ai.js.html deleted file mode 100644 index f693391..0000000 --- a/backend/coverage/lcov-report/backend/routes/ai.js.html +++ /dev/null @@ -1,724 +0,0 @@ - - - - - - Code coverage report for backend/routes/ai.js - - - - - - - - - -
-
-

All files / backend/routes ai.js

-
- -
- 100% - Statements - 58/58 -
- - -
- 68.18% - Branches - 30/44 -
- - -
- 100% - Functions - 8/8 -
- - -
- 100% - Lines - 55/55 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -2146x -6x -6x -6x -6x -  -6x -  -6x -4x -4x -  -4x -1x -  -  -3x -  -  -6x -8x -  -  -  -  -  -  -  -  -8x -  -  -8x -  -  -1x -  -  -  -  -8x -  -  -  -8x -1x -  -  -  -  -7x -  -  -  -  -  -  -  -7x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -7x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -7x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -6x -  -6x -1x -1x -1x -1x -1x -  -1x -  -  -  -  -  -  -  -  -  -5x -  -5x -1x -  -  -  -  -4x -  -3x -  -3x -3x -3x -1x -  -  -  -  -  -  -  -2x -  -  -  -  -6x -2x -  -2x -1x -  -  -1x -  -1x -1x -  -1x -  -  -  -  -  -1x -  -  -1x -  -  -1x -  -  -1x -  -  -  -  -  -  -6x
const express = require("express");
-const Exercise = require("../models/Exercise");
-const { protect } = require("../middleware/authMiddleware");
-const Workout = require("../models/Workout");
-const Split = require("../models/Split");
- 
-const router = express.Router();
- 
-const extractJson = (text) => {
-  const firstBrace = text.indexOf("{");
-  const lastBrace = text.lastIndexOf("}");
- 
-  if (firstBrace === -1 || lastBrace === -1) {
-    throw new Error("LLM response did not contain JSON");
-  }
- 
-  return JSON.parse(text.slice(firstBrace, lastBrace + 1));
-};
- 
-router.post("/generate-workout", protect, async (req, res) => {
-  const profile = req.user.fitnessProfile || {};
- 
-  const {
-    goal = "general_fitness",
-    experienceLevel = "beginner",
-    daysPerWeek = 3,
-    sessionLengthMinutes = 60,
-    equipment = [],
-    limitations = [],
-  } = profile;
- 
-  const equipmentFilter =
-    equipment.length > 0
-      ? {
-          equipment: {
-            $in: equipment.map((item) => new RegExp(item, "i")),
-          },
-        }
-      : {};
- 
-  const exercises = await Exercise.find(equipmentFilter)
-    .limit(40)
-    .select("id name bodyPart equipment target");
- 
-  if (exercises.length === 0) {
-    return res.status(400).json({
-      message: "No matching exercises found for this profile.",
-    });
-  }
- 
-  const exerciseOptions = exercises.map((exercise) => ({
-    id: exercise.id,
-    name: exercise.name,
-    bodyPart: exercise.bodyPart,
-    equipment: exercise.equipment,
-    target: exercise.target,
-  }));
- 
-  const systemPrompt = `
-You are FitForge AI, a workout planning assistant.
- 
-Return ONLY valid JSON.
-Do not use markdown.
-Do not wrap the response in triple backticks.
-Do not include comments.
-Do not include trailing commas.
-Use double quotes for all JSON keys and string values.
-Generate safe, structured workout plans using ONLY the exercises provided.
-Do not invent exercise IDs.
-Do not provide medical advice.
-If limitations are listed, avoid exercises that obviously conflict with them.
-`;
- 
-  const userPrompt = `
-Create a personalized workout plan.
- 
-User profile:
-- Goal: ${goal}
-- Experience level: ${experienceLevel}
-- Days per week: ${daysPerWeek}
-- Session length minutes: ${sessionLengthMinutes}
-- Equipment: ${equipment.join(", ") || "not specified"}
-- Limitations: ${limitations.join(", ") || "none"}
- 
-Available exercises:
-${JSON.stringify(exerciseOptions, null, 2)}
- 
-Return JSON in this exact shape:
-{
-  "programName": "string",
-  "goal": "string",
-  "daysPerWeek": number,
-  "days": [
-    {
-      "day": 1,
-      "name": "string",
-      "focus": "string",
-      "exercises": [
-        {
-          "exerciseId": number,
-          "name": "string",
-          "sets": number,
-          "repRange": "string",
-          "restSeconds": number,
-          "reason": "string"
-        }
-      ]
-    }
-  ],
-  "notes": ["string"]
-}
-`;
- 
-  const llmResponse = await fetch(process.env.LLM_API_URL, {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-      Authorization: `Bearer ${process.env.LLM_API_KEY}`,
-    },
-    body: JSON.stringify({
-      model: process.env.LLM_MODEL,
-      messages: [
-        { role: "system", content: systemPrompt },
-        { role: "user", content: userPrompt },
-      ],
-      temperature: 0.2,
-      response_format: {
-        type: "json_object",
-      },
-    }),
-  });
- 
-  const llmData = await llmResponse.json();
- 
-  if (!llmResponse.ok) {
-    console.error("LLM status:", llmResponse.status);
-    console.error("Retry after:", llmResponse.headers.get("retry-after"));
-    console.error("Rate limit remaining requests:", llmResponse.headers.get("x-ratelimit-remaining-requests"));
-    console.error("Rate limit remaining tokens:", llmResponse.headers.get("x-ratelimit-remaining-tokens"));
-    console.error("LLM request failed:", JSON.stringify(llmData, null, 2));
- 
-    return res.status(llmResponse.status === 429 ? 429 : 500).json({
-      message:
-        llmResponse.status === 429
-          ? "Rate limit hit. Please wait and try again."
-          : "LLM request failed",
-      retryAfter: llmResponse.headers.get("retry-after"),
-      error: llmData,
-    });
-  }
- 
-  const content = llmData.choices?.[0]?.message?.content;
- 
-  if (!content) {
-    return res.status(500).json({
-      message: "LLM returned no content",
-    });
-  }
- 
-  const plan = extractJson(content);
- 
-  const validExerciseIds = new Set(exerciseOptions.map((exercise) => exercise.id));
- 
-  for (const day of plan.days || []) {
-    for (const exercise of day.exercises || []) {
-      if (!validExerciseIds.has(exercise.exerciseId)) {
-        return res.status(500).json({
-          message: "LLM returned an invalid exercise ID",
-          invalidExerciseId: exercise.exerciseId,
-        });
-      }
-    }
-  }
- 
-  res.status(200).json({
-    plan,
-  });
-});
- 
-router.post("/save-generated-plan", protect, async (req, res) => {
-  const { plan } = req.body;
- 
-  if (!plan || !Array.isArray(plan.days)) {
-    return res.status(400).json({ message: "Valid plan is required" });
-  }
- 
-  const createdWorkouts = [];
- 
-  for (const day of plan.days) {
-    const exerciseIds = (day.exercises || []).map((exercise) => exercise.exerciseId);
- 
-    const workout = await Workout.create({
-      userId: req.user._id,
-      name: day.name || `Day ${day.day}`,
-      exercises: exerciseIds,
-    });
- 
-    createdWorkouts.push(workout);
-  }
- 
-  const split = await Split.create({
-    userId: req.user._id,
-    name: plan.programName || "AI Generated Plan",
-    workouts: createdWorkouts.map((workout) => workout._id),
-  });
- 
-  res.status(201).json({
-    message: "AI plan saved successfully",
-    split,
-    workouts: createdWorkouts,
-  });
-});
- 
-module.exports = router;
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/auth.js.html b/backend/coverage/lcov-report/backend/routes/auth.js.html deleted file mode 100644 index 3496fda..0000000 --- a/backend/coverage/lcov-report/backend/routes/auth.js.html +++ /dev/null @@ -1,436 +0,0 @@ - - - - - - Code coverage report for backend/routes/auth.js - - - - - - - - - -
-
-

All files / backend/routes auth.js

-
- -
- 97.72% - Statements - 43/44 -
- - -
- 94.73% - Branches - 18/19 -
- - -
- 100% - Functions - 6/6 -
- - -
- 97.67% - Lines - 42/43 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -1186x -6x -6x -6x -6x -  -6x -  -6x -56x -  -  -  -  -59x -  -  -  -  -  -  -6x -58x -  -58x -1x -  -  -57x -1x -  -  -56x -  -56x -1x -  -  -55x -  -55x -  -  -  -  -  -55x -  -  -  -  -  -  -6x -4x -  -4x -1x -  -  -3x -  -3x -1x -  -  -2x -  -2x -1x -  -  -1x -  -  -  -  -  -6x -1x -  -  -  -  -6x -  -  -  -  -  -  -  -  -2x -  -2x -  -2x -  -  -  -2x -  -  -  -  -  -  -  -  -  -2x -  -2x -  -  -  -6x
const express = require("express");
-const bcrypt = require("bcryptjs");
-const jwt = require("jsonwebtoken");
-const User = require("../models/User");
-const { protect } = require("../middleware/authMiddleware");
- 
-const router = express.Router();
- 
-const createToken = (userId) => {
-    return jwt.sign({ userId }, process.env.JWT_SECRET, {
-        expiresIn: "7d",
-    });
-};
- 
-const serializeUser = (user) => ({
-    id: user._id,
-    name: user.name,
-    email: user.email,
-    fitnessProfile: user.fitnessProfile,
-});
- 
-router.post("/register", async (req, res) => {
-        const { name, email, password } = req.body;
- 
-        if (!name || !email || !password) { 
-            return res.status(400).json({ message: "Name, email, and password are required" });
-        }
- 
-        if (password.length < 8) { 
-            return res.status(400).json({ message: "Password must be at least 8 characters" });
-        }
- 
-        const existingUser = await User.findOne({ email });
- 
-        if (existingUser) {
-            return res.status(409).json({ message: "Email already registered" });
-        }
- 
-        const passwordHash = await bcrypt.hash(password, 12);
- 
-        const user = await User.create({
-            name,
-            email,
-            passwordHash,
-        });
- 
-        res.status(201).json({
-            token: createToken(user._id),
-            user: serializeUser(user),
-        });
- 
-});
- 
-router.post("/login", async (req, res) => { 
-        const { email, password } = req.body;
- 
-        if (!email || !password) {
-            return res.status(400).json({ message: "Email and password are required" });
-        }
- 
-        const user = await User.findOne({ email });
- 
-        if (!user) { 
-            return res.status(401).json({ message: "Invalid email or password" });
-        }
- 
-        const passwordMatches = await bcrypt.compare(password, user.passwordHash);
- 
-        if (!passwordMatches) { 
-            return res.status(401).json({ message: "Invalid email or password" });
-        }
- 
-        res.status(200).json({
-            token: createToken(user._id),
-            user: serializeUser(user),
-        });
-});
- 
-router.get("/me", protect, async (req, res) => {
-    res.status(200).json({
-        user: serializeUser(req.user),
-    });
-});
- 
-router.put("/profile", protect, async (req, res) => {
- 
-        const {
-            goal,
-            experienceLevel,
-            daysPerWeek,
-            sessionLengthMinutes,
-            equipment,
-            limitations,
-        } = req.body;
- 
-        const user = await User.findById(req.user._id);
- 
-        Iif (!user) {
-            return res.status(400).json({ message: "User not found" });
-        }
- 
-        user.fitnessProfile = {
-            ...user.fitnessProfile,
-            goal,
-            experienceLevel,
-            daysPerWeek,
-            sessionLengthMinutes,
-            equipment,
-            limitations,
-        };
- 
-        await user.save();
- 
-        res.status(200).json({ user: serializeUser(user) });
- 
-});
- 
-module.exports = router;
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/exercises.js.html b/backend/coverage/lcov-report/backend/routes/exercises.js.html deleted file mode 100644 index 9652311..0000000 --- a/backend/coverage/lcov-report/backend/routes/exercises.js.html +++ /dev/null @@ -1,181 +0,0 @@ - - - - - - Code coverage report for backend/routes/exercises.js - - - - - - - - - -
-
-

All files / backend/routes exercises.js

-
- -
- 100% - Statements - 11/11 -
- - -
- 100% - Branches - 4/4 -
- - -
- 100% - Functions - 1/1 -
- - -
- 100% - Lines - 11/11 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -336x -6x -  -6x -  -  -6x -3x -  -3x -3x -1x -  -  -  -  -  -  -  -  -  -3x -  -  -  -  -  -  -  -3x -  -  -6x
const express = require("express");
-const Exercise = require("../models/Exercise");
- 
-const router = express.Router();
- 
-// Get exercises with optional search and pagination
-router.get("/", async (req, res) => {
-  const { search, page = 1, limit = 100 } = req.query;
- 
-  let query = {};
-  if (search) {
-    query = {
-      $or: [
-        { name: { $regex: search, $options: "i" } },
-        { bodyPart: { $regex: search, $options: "i" } },
-        { equipment: { $regex: search, $options: "i" } },
-        { target: { $regex: search, $options: "i" } },
-      ],
-    };
-  }
- 
-    const exercises = await Exercise.aggregate([
-      { $match: query },
-      { $group: { _id: "$name", doc: { $first: "$$ROOT" } } },
-      { $replaceRoot: { newRoot: "$doc" } },
-      { $sort: { name: 1 } },
-      { $skip: (page - 1) * limit },
-      { $limit: parseInt(limit) },
-    ]);
-    res.status(200).json(exercises);
-});
- 
-module.exports = router;
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/index.html b/backend/coverage/lcov-report/backend/routes/index.html deleted file mode 100644 index 4419116..0000000 --- a/backend/coverage/lcov-report/backend/routes/index.html +++ /dev/null @@ -1,176 +0,0 @@ - - - - - - Code coverage report for backend/routes - - - - - - - - - -
-
-

All files backend/routes

-
- -
- 99.51% - Statements - 207/208 -
- - -
- 84.34% - Branches - 97/115 -
- - -
- 100% - Functions - 31/31 -
- - -
- 99.5% - Lines - 200/201 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
ai.js -
-
100%58/5868.18%30/44100%8/8100%55/55
auth.js -
-
97.72%43/4494.73%18/19100%6/697.67%42/43
exercises.js -
-
100%11/11100%4/4100%1/1100%11/11
splits.js -
-
100%54/5488.46%23/26100%10/10100%51/51
workouts.js -
-
100%41/41100%22/22100%6/6100%41/41
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/splits.js.html b/backend/coverage/lcov-report/backend/routes/splits.js.html deleted file mode 100644 index 0b4bc6d..0000000 --- a/backend/coverage/lcov-report/backend/routes/splits.js.html +++ /dev/null @@ -1,472 +0,0 @@ - - - - - - Code coverage report for backend/routes/splits.js - - - - - - - - - -
-
-

All files / backend/routes splits.js

-
- -
- 100% - Statements - 54/54 -
- - -
- 88.46% - Branches - 23/26 -
- - -
- 100% - Functions - 10/10 -
- - -
- 100% - Lines - 51/51 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -1306x -6x -6x -6x -  -6x -  -  -6x -12x -  -12x -1x -  -  -11x -1x -  -  -10x -  -  -  -  -  -10x -  -10x -  -10x -  -  -  -6x -1x -1x -  -  -  -  -6x -2x -  -  -  -  -2x -1x -  -  -1x -1x -  -1x -1x -1x -1x -  -1x -  -  -  -1x -  -  -  -6x -3x -  -  -  -  -3x -2x -  -  -1x -  -  -  -6x -4x -  -4x -1x -  -  -3x -1x -  -  -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -2x -1x -  -  -1x -  -  -  -  -6x -2x -  -  -  -  -2x -1x -  -  -1x -  -  -6x - 
const express = require("express");
-const Split = require("../models/Split");
-const Exercise = require("../models/Exercise");
-const { protect } = require("../middleware/authMiddleware");
- 
-const router = express.Router();
- 
-// Create a new split
-router.post("/", protect, async (req, res) => {
-  const { name, workouts } = req.body;
- 
-  if (!name || !name.trim()) {
-    return res.status(400).json({ message: "Split name is required" });
-  }
- 
-  if (!Array.isArray(workouts)) {
-    return res.status(400).json({ message: "Workouts must be an array" });
-  }
- 
-    const newSplit = new Split({
-      userId: req.user._id,
-      name: name.trim(),
-      workouts,
-    });
- 
-    await newSplit.save();
- 
-    const populatedSplit = await Split.findById(newSplit._id).populate("workouts");
- 
-    res.status(201).json(populatedSplit);
-});
- 
-// Fetch all splits
-router.get("/", protect, async (req, res) => {
-    const splits = await Split.find({ userId: req.user._id }).populate("workouts"); // Populate workouts
-    res.status(200).json(splits); // Return all splits
- 
-});
- 
-// Fetch a split with full exercise details per workout (for the detail modal)
-router.get("/:id/details", protect, async (req, res) => {
-    const split = await Split.findOne({
-      _id: req.params.id,
-      userId: req.user._id,
-    }).populate("workouts");
- 
-    if (!split) {
-      return res.status(404).json({ message: "Split not found" });
-    }
- 
-    const splitObj = split.toObject();
-    const exerciseIds = splitObj.workouts.flatMap((w) => w.exercises || []);
- 
-    Eif (exerciseIds.length > 0) {
-      const exercises = await Exercise.find({ id: { $in: exerciseIds } });
-      const byId = new Map(exercises.map((e) => [e.id, e]));
-      splitObj.workouts = splitObj.workouts.map((w) => ({
-        ...w,
-        exercises: (w.exercises || []).map((id) => byId.get(id)).filter(Boolean),
-      }));
-    }
- 
-    res.status(200).json(splitObj);
-});
- 
-// Fetch a specific split by ID
-router.get("/:id", protect, async (req, res) => {
-    const split = await Split.findOne({
-      _id: req.params.id,
-      userId: req.user._id,
-    }).populate("workouts");
- 
-    if (!split) {
-      return res.status(404).json({ message: "Split not found" }); // Handle not found
-    }
- 
-    res.status(200).json(split); // Return the found split
-});
- 
-// Update a specific split by ID
-router.put("/:id", protect, async (req, res) => {
-  const { name, workouts } = req.body;
- 
-  if (!name || !name.trim()) {
-    return res.status(400).json({ message: "Split name is required" });
-  }
- 
-  if (!Array.isArray(workouts)) {
-    return res.status(400).json({ message: "Workouts must be an array" });
-  }
- 
-    const split = await Split.findOneAndUpdate(
-      {
-        _id: req.params.id,
-        userId: req.user._id,
-      },
-      {
-        name: name.trim(),
-        workouts,
-      },
-      {
-        new: true,
-        runValidators: true,
-      }
-    ).populate("workouts");
- 
-    if (!split) {
-      return res.status(404).json({ message: "Split not found" });
-    }
- 
-    res.status(200).json(split);
- 
-});
- 
-// Delete a specific split by ID
-router.delete("/:id", protect, async (req, res) => {
-    const split = await Split.findOneAndDelete({
-      _id: req.params.id,
-      userId: req.user._id,
-    });
- 
-    if (!split) {
-      return res.status(404).json({ message: "Split not found" }); // Handle not found
-    }
- 
-    res.status(200).json({ message: "Split deleted successfully" }); // Return success message
-});
- 
-module.exports = router; // Export the router
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/backend/routes/workouts.js.html b/backend/coverage/lcov-report/backend/routes/workouts.js.html deleted file mode 100644 index d7ecdc6..0000000 --- a/backend/coverage/lcov-report/backend/routes/workouts.js.html +++ /dev/null @@ -1,436 +0,0 @@ - - - - - - Code coverage report for backend/routes/workouts.js - - - - - - - - - -
-
-

All files / backend/routes workouts.js

-
- -
- 100% - Statements - 41/41 -
- - -
- 100% - Branches - 22/22 -
- - -
- 100% - Functions - 6/6 -
- - -
- 100% - Lines - 41/41 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-

-
1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -1186x -6x -6x -6x -  -6x -  -  -6x -15x -  -  -15x -1x -  -  -14x -2x -  -  -  -12x -  -  -  -  -12x -12x -  -  -  -  -6x -1x -  -  -1x -  -1x -  -  -1x -  -  -  -  -  -  -1x -  -  -  -6x -  -6x -  -  -  -  -5x -3x -  -2x -  -  -  -6x -4x -  -4x -1x -  -  -3x -1x -  -  -  -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -2x -1x -  -  -1x -  -  -  -6x -  -2x -  -  -  -  -2x -1x -  -  -1x -  -  -  -6x - 
const express = require("express");
-const Exercise = require("../models/Exercise");
-const Workout = require("../models/Workout");
-const { protect } = require("../middleware/authMiddleware");
- 
-const router = express.Router();
- 
-// Create a new workout
-router.post("/", protect, async (req, res) => {
-  const { name, exercises } = req.body;
- 
-  // Validate input
-  if (!name || !name.trim()) {
-    return res.status(400).json({ message: "Workout name is required" });
-  }
- 
-  if (!Array.isArray(exercises) || exercises.length === 0) {
-    return res.status(400).json({ message: "Please select at least one exercise" });
-  }
- 
- 
-    const workout = new Workout({
-      userId: req.user._id,
-      name,
-      exercises,
-    });
-    await workout.save();
-    res.status(201).json(workout); // Return the created workout
- 
-});
- 
-// Fetch all workouts
-router.get("/", protect, async (req, res) => {
-    const workouts = await Workout.find({ userId: req.user._id });
- 
-    // Populate exercises for each workout
-    const populatedWorkouts = await Promise.all(
-      workouts.map(async (workout) => {
-        const exercises = await Exercise.find({
-          id: { $in: workout.exercises },
-        });
-        return {
-          ...workout.toObject(),
-          exercises,
-        };
-      })
-    );
- 
-    res.status(200).json(populatedWorkouts); // Return all workouts with exercises
-});
- 
-// Fetch a specific workout by ID
-router.get("/:id", protect, async (req, res) => {
- 
-    const workout = await Workout.findOne({
-      _id: req.params.id,
-      userId: req.user._id,
-    });
- 
-    if (!workout) {
-      return res.status(404).json({ message: "Workout not found" });
-    }
-    res.status(200).json(workout); // Return the found workout
-});
- 
-// Update a specific workout by ID
-router.put("/:id", protect, async (req, res) => {
-  const { name, exercises } = req.body;
-  
-  if (!name || !name.trim()) {
-    return res.status(400).json({ message: "Workout name is required" });
-  }
- 
-  if (!Array.isArray(exercises) || exercises.length === 0) {
-    return res.status(400).json({ message: "Please select at least one exercise" });
-  }
- 
- 
-    const workout = await Workout.findOneAndUpdate(
-      {
-        _id: req.params.id,
-        userId: req.user._id,
-      },
-      {
-        name,
-        exercises,
-      },
-      {
-        new: true,
-        runValidators: true,
-      }
-    );
- 
-    if (!workout) {
-      return res.status(404).json({ message: "Workout not found" });
-    }
- 
-    res.status(200).json(workout); // Return the updated workout
-});
- 
-// Delete a specific workout by ID
-router.delete("/:id", protect, async (req, res) => {
- 
-    const workout = await Workout.findOneAndDelete({
-      _id: req.params.id,
-      userId: req.user._id,
-    });
- 
-    if (!workout) {
-      return res.status(404).json({ message: "Workout not found" });
-    }
- 
-    res.status(200).json({ message: "Workout deleted successfully" }); // Return success message
- 
-});
- 
-module.exports = router; // Export the router
- 
- -
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/base.css b/backend/coverage/lcov-report/base.css deleted file mode 100644 index f418035..0000000 --- a/backend/coverage/lcov-report/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/backend/coverage/lcov-report/block-navigation.js b/backend/coverage/lcov-report/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/backend/coverage/lcov-report/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/backend/coverage/lcov-report/favicon.png b/backend/coverage/lcov-report/favicon.png deleted file mode 100644 index c1525b811a167671e9de1fa78aab9f5c0b61cef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmV;u0Yd(XP))rP{nL}Ln%S7`m{0DjX9TLF* zFCb$4Oi7vyLOydb!7n&^ItCzb-%BoB`=x@N2jll2Nj`kauio%aw_@fe&*}LqlFT43 z8doAAe))z_%=P%v^@JHp3Hjhj^6*Kr_h|g_Gr?ZAa&y>wxHE99Gk>A)2MplWz2xdG zy8VD2J|Uf#EAw*bo5O*PO_}X2Tob{%bUoO2G~T`@%S6qPyc}VkhV}UifBuRk>%5v( z)x7B{I~z*k<7dv#5tC+m{km(D087J4O%+<<;K|qwefb6@GSX45wCK}Sn*> - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- 99.64% - Statements - 283/284 -
- - -
- 86.25% - Branches - 113/131 -
- - -
- 100% - Functions - 37/37 -
- - -
- 99.63% - Lines - 274/275 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
backend -
-
100%33/33100%6/6100%3/3100%32/32
backend/middleware -
-
100%31/31100%10/10100%3/3100%30/30
backend/models -
-
100%12/12100%0/0100%0/0100%12/12
backend/routes -
-
99.51%207/20884.34%97/115100%31/3199.5%200/201
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/backend/coverage/lcov-report/prettify.css b/backend/coverage/lcov-report/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/backend/coverage/lcov-report/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/backend/coverage/lcov-report/prettify.js b/backend/coverage/lcov-report/prettify.js deleted file mode 100644 index b322523..0000000 --- a/backend/coverage/lcov-report/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/backend/coverage/lcov-report/sort-arrow-sprite.png b/backend/coverage/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316eb3f65dec9063332d2f69bf3093bbfab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0vp^>_9Bd!3HEZxJ@+%Qh}Z>jv*C{$p!i!8j}?a+@3A= zIAGwzjijN=FBi!|L1t?LM;Q;gkwn>2cAy-KV{dn nf0J1DIvEHQu*n~6U}x}qyky7vi4|9XhBJ7&`njxgN@xNA8m%nc diff --git a/backend/coverage/lcov-report/sorter.js b/backend/coverage/lcov-report/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/backend/coverage/lcov-report/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/backend/coverage/lcov.info b/backend/coverage/lcov.info deleted file mode 100644 index a20f4cd..0000000 --- a/backend/coverage/lcov.info +++ /dev/null @@ -1,597 +0,0 @@ -TN: -SF:app.js -FN:19,(anonymous_0) -FN:25,(anonymous_1) -FN:42,(anonymous_2) -FNF:3 -FNH:3 -FNDA:6,(anonymous_0) -FNDA:143,(anonymous_1) -FNDA:1,(anonymous_2) -DA:1,6 -DA:2,6 -DA:3,6 -DA:4,6 -DA:5,6 -DA:6,6 -DA:8,6 -DA:9,6 -DA:10,6 -DA:11,6 -DA:12,6 -DA:13,6 -DA:15,6 -DA:17,6 -DA:19,6 -DA:22,6 -DA:23,6 -DA:26,143 -DA:27,1 -DA:31,6 -DA:33,6 -DA:34,6 -DA:35,6 -DA:36,6 -DA:37,6 -DA:38,6 -DA:41,6 -DA:42,6 -DA:43,1 -DA:46,6 -DA:47,6 -DA:49,6 -LF:32 -LH:32 -BRDA:17,0,0,6 -BRDA:17,0,1,6 -BRDA:26,1,0,142 -BRDA:26,1,1,1 -BRDA:26,2,0,143 -BRDA:26,2,1,1 -BRF:6 -BRH:6 -end_of_record -TN: -SF:middleware/authMiddleware.js -FN:4,(anonymous_0) -FNF:1 -FNH:1 -FNDA:76,(anonymous_0) -DA:1,6 -DA:2,6 -DA:4,6 -DA:5,76 -DA:6,76 -DA:7,76 -DA:8,8 -DA:10,68 -DA:11,68 -DA:12,66 -DA:13,66 -DA:14,1 -DA:16,65 -DA:17,65 -DA:19,2 -DA:23,6 -LF:16 -LH:16 -BRDA:7,0,0,8 -BRDA:7,0,1,68 -BRDA:7,1,0,76 -BRDA:7,1,1,69 -BRDA:13,2,0,1 -BRDA:13,2,1,65 -BRF:6 -BRH:6 -end_of_record -TN: -SF:middleware/errorHandler.js -FN:2,(anonymous_0) -FNF:1 -FNH:1 -FNDA:7,(anonymous_0) -DA:2,7 -DA:3,7 -DA:4,2 -DA:6,5 -DA:7,1 -DA:9,4 -DA:10,4 -DA:13,7 -LF:8 -LH:8 -BRDA:3,0,0,2 -BRDA:3,0,1,5 -BRDA:6,1,0,1 -BRDA:6,1,1,4 -BRF:4 -BRH:4 -end_of_record -TN: -SF:middleware/rateLimiters.js -FN:4,(anonymous_0) -FNF:1 -FNH:1 -FNDA:223,(anonymous_0) -DA:1,6 -DA:4,223 -DA:6,6 -DA:15,6 -DA:24,6 -DA:33,6 -LF:6 -LH:6 -BRF:0 -BRH:0 -end_of_record -TN: -SF:models/Exercise.js -FNF:0 -FNH:0 -DA:1,6 -DA:3,6 -DA:12,6 -LF:3 -LH:3 -BRF:0 -BRH:0 -end_of_record -TN: -SF:models/Split.js -FNF:0 -FNH:0 -DA:1,6 -DA:3,6 -DA:25,6 -LF:3 -LH:3 -BRF:0 -BRH:0 -end_of_record -TN: -SF:models/User.js -FNF:0 -FNH:0 -DA:1,6 -DA:3,6 -DA:53,6 -LF:3 -LH:3 -BRF:0 -BRH:0 -end_of_record -TN: -SF:models/Workout.js -FNF:0 -FNH:0 -DA:1,6 -DA:3,6 -DA:25,6 -LF:3 -LH:3 -BRF:0 -BRH:0 -end_of_record -TN: -SF:routes/ai.js -FN:9,(anonymous_0) -FN:20,(anonymous_1) -FN:36,(anonymous_2) -FN:51,(anonymous_3) -FN:162,(anonymous_4) -FN:180,(anonymous_5) -FN:190,(anonymous_6) -FN:204,(anonymous_7) -FNF:8 -FNH:8 -FNDA:4,(anonymous_0) -FNDA:8,(anonymous_1) -FNDA:1,(anonymous_2) -FNDA:7,(anonymous_3) -FNDA:3,(anonymous_4) -FNDA:2,(anonymous_5) -FNDA:1,(anonymous_6) -FNDA:1,(anonymous_7) -DA:1,6 -DA:2,6 -DA:3,6 -DA:4,6 -DA:5,6 -DA:7,6 -DA:9,6 -DA:10,4 -DA:11,4 -DA:13,4 -DA:14,1 -DA:17,3 -DA:20,6 -DA:21,8 -DA:30,8 -DA:33,8 -DA:36,1 -DA:41,8 -DA:45,8 -DA:46,1 -DA:51,7 -DA:59,7 -DA:74,7 -DA:114,7 -DA:133,6 -DA:135,6 -DA:136,1 -DA:137,1 -DA:138,1 -DA:139,1 -DA:140,1 -DA:142,1 -DA:152,5 -DA:154,5 -DA:155,1 -DA:160,4 -DA:162,3 -DA:164,3 -DA:165,3 -DA:166,3 -DA:167,1 -DA:175,2 -DA:180,6 -DA:181,2 -DA:183,2 -DA:184,1 -DA:187,1 -DA:189,1 -DA:190,1 -DA:192,1 -DA:198,1 -DA:201,1 -DA:204,1 -DA:207,1 -DA:214,6 -LF:55 -LH:55 -BRDA:13,0,0,1 -BRDA:13,0,1,3 -BRDA:13,1,0,4 -BRDA:13,1,1,3 -BRDA:21,2,0,8 -BRDA:21,2,1,0 -BRDA:24,3,0,0 -BRDA:25,4,0,0 -BRDA:26,5,0,0 -BRDA:27,6,0,0 -BRDA:28,7,0,0 -BRDA:29,8,0,0 -BRDA:33,9,0,1 -BRDA:33,9,1,7 -BRDA:45,10,0,1 -BRDA:45,10,1,7 -BRDA:82,11,0,7 -BRDA:82,11,1,6 -BRDA:83,12,0,7 -BRDA:83,12,1,7 -BRDA:135,13,0,1 -BRDA:135,13,1,5 -BRDA:142,14,0,1 -BRDA:142,14,1,0 -BRDA:144,15,0,1 -BRDA:144,15,1,0 -BRDA:154,16,0,1 -BRDA:154,16,1,4 -BRDA:164,17,0,3 -BRDA:164,17,1,0 -BRDA:165,18,0,3 -BRDA:165,18,1,0 -BRDA:166,19,0,1 -BRDA:166,19,1,2 -BRDA:183,20,0,1 -BRDA:183,20,1,1 -BRDA:183,21,0,2 -BRDA:183,21,1,2 -BRDA:190,22,0,1 -BRDA:190,22,1,0 -BRDA:194,23,0,1 -BRDA:194,23,1,0 -BRDA:203,24,0,1 -BRDA:203,24,1,0 -BRF:44 -BRH:30 -end_of_record -TN: -SF:routes/auth.js -FN:9,(anonymous_0) -FN:15,(anonymous_1) -FN:22,(anonymous_2) -FN:54,(anonymous_3) -FN:79,(anonymous_4) -FN:85,(anonymous_5) -FNF:6 -FNH:6 -FNDA:56,(anonymous_0) -FNDA:59,(anonymous_1) -FNDA:58,(anonymous_2) -FNDA:4,(anonymous_3) -FNDA:1,(anonymous_4) -FNDA:2,(anonymous_5) -DA:1,6 -DA:2,6 -DA:3,6 -DA:4,6 -DA:5,6 -DA:7,6 -DA:9,6 -DA:10,56 -DA:15,59 -DA:22,6 -DA:23,58 -DA:25,58 -DA:26,1 -DA:29,57 -DA:30,1 -DA:33,56 -DA:35,56 -DA:36,1 -DA:39,55 -DA:41,55 -DA:47,55 -DA:54,6 -DA:55,4 -DA:57,4 -DA:58,1 -DA:61,3 -DA:63,3 -DA:64,1 -DA:67,2 -DA:69,2 -DA:70,1 -DA:73,1 -DA:79,6 -DA:80,1 -DA:85,6 -DA:94,2 -DA:96,2 -DA:98,2 -DA:99,0 -DA:102,2 -DA:112,2 -DA:114,2 -DA:118,6 -LF:43 -LH:42 -BRDA:25,0,0,1 -BRDA:25,0,1,57 -BRDA:25,1,0,58 -BRDA:25,1,1,57 -BRDA:25,1,2,57 -BRDA:29,2,0,1 -BRDA:29,2,1,56 -BRDA:35,3,0,1 -BRDA:35,3,1,55 -BRDA:57,4,0,1 -BRDA:57,4,1,3 -BRDA:57,5,0,4 -BRDA:57,5,1,4 -BRDA:63,6,0,1 -BRDA:63,6,1,2 -BRDA:69,7,0,1 -BRDA:69,7,1,1 -BRDA:98,8,0,0 -BRDA:98,8,1,2 -BRF:19 -BRH:18 -end_of_record -TN: -SF:routes/exercises.js -FN:7,(anonymous_0) -FNF:1 -FNH:1 -FNDA:3,(anonymous_0) -DA:1,6 -DA:2,6 -DA:4,6 -DA:7,6 -DA:8,3 -DA:10,3 -DA:11,3 -DA:12,1 -DA:22,3 -DA:30,3 -DA:33,6 -LF:11 -LH:11 -BRDA:8,0,0,2 -BRDA:8,1,0,2 -BRDA:11,2,0,1 -BRDA:11,2,1,2 -BRF:4 -BRH:4 -end_of_record -TN: -SF:routes/splits.js -FN:9,(anonymous_0) -FN:34,(anonymous_1) -FN:41,(anonymous_2) -FN:52,(anonymous_3) -FN:56,(anonymous_4) -FN:57,(anonymous_5) -FN:59,(anonymous_6) -FN:67,(anonymous_7) -FN:81,(anonymous_8) -FN:116,(anonymous_9) -FNF:10 -FNH:10 -FNDA:12,(anonymous_0) -FNDA:1,(anonymous_1) -FNDA:2,(anonymous_2) -FNDA:1,(anonymous_3) -FNDA:1,(anonymous_4) -FNDA:1,(anonymous_5) -FNDA:1,(anonymous_6) -FNDA:3,(anonymous_7) -FNDA:4,(anonymous_8) -FNDA:2,(anonymous_9) -DA:1,6 -DA:2,6 -DA:3,6 -DA:4,6 -DA:6,6 -DA:9,6 -DA:10,12 -DA:12,12 -DA:13,1 -DA:16,11 -DA:17,1 -DA:20,10 -DA:26,10 -DA:28,10 -DA:30,10 -DA:34,6 -DA:35,1 -DA:36,1 -DA:41,6 -DA:42,2 -DA:47,2 -DA:48,1 -DA:51,1 -DA:52,1 -DA:54,1 -DA:55,1 -DA:56,1 -DA:57,1 -DA:59,1 -DA:63,1 -DA:67,6 -DA:68,3 -DA:73,3 -DA:74,2 -DA:77,1 -DA:81,6 -DA:82,4 -DA:84,4 -DA:85,1 -DA:88,3 -DA:89,1 -DA:92,2 -DA:107,2 -DA:108,1 -DA:111,1 -DA:116,6 -DA:117,2 -DA:122,2 -DA:123,1 -DA:126,1 -DA:129,6 -LF:51 -LH:51 -BRDA:12,0,0,1 -BRDA:12,0,1,11 -BRDA:12,1,0,12 -BRDA:12,1,1,11 -BRDA:16,2,0,1 -BRDA:16,2,1,10 -BRDA:47,3,0,1 -BRDA:47,3,1,1 -BRDA:52,4,0,1 -BRDA:52,4,1,0 -BRDA:54,5,0,1 -BRDA:54,5,1,0 -BRDA:59,6,0,1 -BRDA:59,6,1,0 -BRDA:73,7,0,2 -BRDA:73,7,1,1 -BRDA:84,8,0,1 -BRDA:84,8,1,3 -BRDA:84,9,0,4 -BRDA:84,9,1,3 -BRDA:88,10,0,1 -BRDA:88,10,1,2 -BRDA:107,11,0,1 -BRDA:107,11,1,1 -BRDA:122,12,0,1 -BRDA:122,12,1,1 -BRF:26 -BRH:23 -end_of_record -TN: -SF:routes/workouts.js -FN:9,(anonymous_0) -FN:33,(anonymous_1) -FN:38,(anonymous_2) -FN:53,(anonymous_3) -FN:67,(anonymous_4) -FN:102,(anonymous_5) -FNF:6 -FNH:6 -FNDA:15,(anonymous_0) -FNDA:1,(anonymous_1) -FNDA:1,(anonymous_2) -FNDA:6,(anonymous_3) -FNDA:4,(anonymous_4) -FNDA:2,(anonymous_5) -DA:1,6 -DA:2,6 -DA:3,6 -DA:4,6 -DA:6,6 -DA:9,6 -DA:10,15 -DA:13,15 -DA:14,1 -DA:17,14 -DA:18,2 -DA:22,12 -DA:27,12 -DA:28,12 -DA:33,6 -DA:34,1 -DA:37,1 -DA:39,1 -DA:42,1 -DA:49,1 -DA:53,6 -DA:55,6 -DA:60,5 -DA:61,3 -DA:63,2 -DA:67,6 -DA:68,4 -DA:70,4 -DA:71,1 -DA:74,3 -DA:75,1 -DA:79,2 -DA:94,2 -DA:95,1 -DA:98,1 -DA:102,6 -DA:104,2 -DA:109,2 -DA:110,1 -DA:113,1 -DA:117,6 -LF:41 -LH:41 -BRDA:13,0,0,1 -BRDA:13,0,1,14 -BRDA:13,1,0,15 -BRDA:13,1,1,14 -BRDA:17,2,0,2 -BRDA:17,2,1,12 -BRDA:17,3,0,14 -BRDA:17,3,1,13 -BRDA:60,4,0,3 -BRDA:60,4,1,2 -BRDA:70,5,0,1 -BRDA:70,5,1,3 -BRDA:70,6,0,4 -BRDA:70,6,1,3 -BRDA:74,7,0,1 -BRDA:74,7,1,2 -BRDA:74,8,0,3 -BRDA:74,8,1,3 -BRDA:94,9,0,1 -BRDA:94,9,1,1 -BRDA:109,10,0,1 -BRDA:109,10,1,1 -BRF:22 -BRH:22 -end_of_record