diff --git a/api/controllers/v1/caver/add-explored-cave.js b/api/controllers/v1/caver/add-explored-entrance.js similarity index 53% rename from api/controllers/v1/caver/add-explored-cave.js rename to api/controllers/v1/caver/add-explored-entrance.js index 03bbcbc3a..9be63fb3d 100644 --- a/api/controllers/v1/caver/add-explored-cave.js +++ b/api/controllers/v1/caver/add-explored-entrance.js @@ -2,7 +2,7 @@ const RightService = require('../../../services/RightService'); module.exports = async (req, res) => { const caverId = req.param('caverId'); - const caveId = req.param('caveId'); + const entranceId = req.param('entranceId'); const hasAdminRight = RightService.hasGroup( req.token?.groups, @@ -18,12 +18,12 @@ module.exports = async (req, res) => { !hasModeratorRight && req.token.id !== parseInt(caverId, 10) ) { - return res.forbidden('You can only manage your own explored caves.'); + return res.forbidden('You can only manage your own explored entrances.'); } - const cave = await TCave.findOne({ id: caveId }); - if (!cave || cave.isDeleted) { - return res.notFound(`Cave with id ${caveId} not found.`); + const entrance = await TEntrance.findOne({ id: entranceId }); + if (!entrance || entrance.isDeleted) { + return res.notFound(`Entrance with id ${entranceId} not found.`); } const caver = await TCaver.findOne({ id: caverId }); @@ -31,26 +31,22 @@ module.exports = async (req, res) => { return res.notFound(`Caver with id ${caverId} not found.`); } - // Check if a relationship already exists - const existingQuery = ` - SELECT 1 FROM j_caver_cave_explorer - WHERE id_cave = $1 AND id_caver = $2 + // Atomic upsert: INSERT with ON CONFLICT eliminates the TOCTOU race + const insertQuery = ` + INSERT INTO j_caver_entrance_explorer (id_entrance, id_caver) + VALUES ($1, $2) + ON CONFLICT (id_entrance, id_caver) DO NOTHING `; - const existingResult = await sails.sendNativeQuery(existingQuery, [ - caveId, + const result = await sails.sendNativeQuery(insertQuery, [ + entranceId, caverId, ]); - if (existingResult.rows.length > 0) { - return res.badRequest('Caver is already exploring this cave.'); + if (result.rowCount === 0) { + return res.conflict( + 'Caver is already registered as an explorer of this entrance.' + ); } - // Create the relationship - const insertQuery = ` - INSERT INTO j_caver_cave_explorer (id_cave, id_caver) - VALUES ($1, $2) - `; - await sails.sendNativeQuery(insertQuery, [caveId, caverId]); - return res.status(204).send(); }; diff --git a/api/controllers/v1/caver/delete.js b/api/controllers/v1/caver/delete.js index a020f99d9..c7b7e7b78 100644 --- a/api/controllers/v1/caver/delete.js +++ b/api/controllers/v1/caver/delete.js @@ -2,6 +2,7 @@ const ControllerService = require('../../../services/ControllerService'); const RightService = require('../../../services/RightService'); const { toCaver } = require('../../../services/mapping/converters'); const CaverService = require('../../../services/CaverService'); +const CommonService = require('../../../services/CommonService'); const DEFAULT_DELETED_CAVER_ID = 8; @@ -38,7 +39,7 @@ module.exports = async (req, res) => { let mergeIntoEntity; if (shouldMergeInto) { mergeIntoEntity = await TCaver.findOne(mergeIntoId) - .populate('exploredCaves') + .populate('exploredEntrances') .populate('groups') .populate('subscribedToCountries') .populate('subscribedToMassifs') @@ -150,13 +151,18 @@ module.exports = async (req, res) => { : null; await Promise.all([ - linkedEntitiesDeleteOrMerge('exploredCaves'), + linkedEntitiesDeleteOrMerge('exploredEntrances'), linkedEntitiesDeleteOrMerge('groups'), linkedEntitiesDeleteOrMerge('subscribedToCountries'), linkedEntitiesDeleteOrMerge('subscribedToMassifs'), linkedEntitiesDeleteOrMerge('subscribedToRegions'), linkedEntitiesDeleteOrMerge('grottos'), linkedEntitiesDeleteOrMerge('documents', allDocumentIds), + // Clean up legacy j_caver_cave_explorer rows (table preserved, no Waterline association) + CommonService.query( + 'DELETE FROM j_caver_cave_explorer WHERE id_caver = $1', + [caverId] + ), ]); await TCaver.destroyOne({ id: caverId }); diff --git a/api/controllers/v1/caver/remove-explored-cave.js b/api/controllers/v1/caver/remove-explored-entrance.js similarity index 62% rename from api/controllers/v1/caver/remove-explored-cave.js rename to api/controllers/v1/caver/remove-explored-entrance.js index 2d69e0c37..deede3d9d 100644 --- a/api/controllers/v1/caver/remove-explored-cave.js +++ b/api/controllers/v1/caver/remove-explored-entrance.js @@ -2,7 +2,7 @@ const RightService = require('../../../services/RightService'); module.exports = async (req, res) => { const caverId = req.param('caverId'); - const caveId = req.param('caveId'); + const entranceId = req.param('entranceId'); const hasAdminRight = RightService.hasGroup( req.token?.groups, @@ -18,12 +18,12 @@ module.exports = async (req, res) => { !hasModeratorRight && req.token.id !== parseInt(caverId, 10) ) { - return res.forbidden('You can only manage your own explored caves.'); + return res.forbidden('You can only manage your own explored entrances.'); } - const cave = await TCave.findOne({ id: caveId }); - if (!cave || cave.isDeleted) { - return res.notFound(`Cave with id ${caveId} not found.`); + const entrance = await TEntrance.findOne({ id: entranceId }); + if (!entrance || entrance.isDeleted) { + return res.notFound(`Entrance with id ${entranceId} not found.`); } const caver = await TCaver.findOne({ id: caverId }); @@ -33,23 +33,25 @@ module.exports = async (req, res) => { // Check if a relationship exists const existingQuery = ` - SELECT 1 FROM j_caver_cave_explorer - WHERE id_cave = $1 AND id_caver = $2 + SELECT 1 FROM j_caver_entrance_explorer + WHERE id_entrance = $1 AND id_caver = $2 `; const existingResult = await sails.sendNativeQuery(existingQuery, [ - caveId, + entranceId, caverId, ]); if (existingResult.rows.length === 0) { - return res.badRequest('Caver is not exploring this cave.'); + return res.notFound( + 'Caver is not registered as an explorer of this entrance.' + ); } // Remove the relationship const deleteQuery = ` - DELETE FROM j_caver_cave_explorer - WHERE id_cave = $1 AND id_caver = $2 + DELETE FROM j_caver_entrance_explorer + WHERE id_entrance = $1 AND id_caver = $2 `; - await sails.sendNativeQuery(deleteQuery, [caveId, caverId]); + await sails.sendNativeQuery(deleteQuery, [entranceId, caverId]); return res.status(204).send(); }; diff --git a/api/models/JCaverEntranceExplorer.js b/api/models/JCaverEntranceExplorer.js new file mode 100644 index 000000000..610b47727 --- /dev/null +++ b/api/models/JCaverEntranceExplorer.js @@ -0,0 +1,22 @@ +/** + * JCaverEntranceExplorer.js + * + * @description :: jCaverEntranceExplorer model + * @docs :: http://sailsjs.org/#!documentation/models + */ + +module.exports = { + tableName: 'j_caver_entrance_explorer', + + attributes: { + caver: { + columnName: 'id_caver', + model: 'TCaver', + }, + + entrance: { + columnName: 'id_entrance', + model: 'TEntrance', + }, + }, +}; diff --git a/api/models/TCave.js b/api/models/TCave.js index e916f57be..5d17cbc56 100644 --- a/api/models/TCave.js +++ b/api/models/TCave.js @@ -128,12 +128,6 @@ module.exports = { through: 'JGrottoCaveExplorer', }, - explorerCavers: { - collection: 'TCaver', - via: 'cave', - through: 'JCaverCaveExplorer', - }, - partneringGrottos: { collection: 'TGrotto', via: 'cave', diff --git a/api/models/TCaver.js b/api/models/TCaver.js index 0043ffac9..5af16efaa 100644 --- a/api/models/TCaver.js +++ b/api/models/TCaver.js @@ -191,10 +191,10 @@ module.exports = { through: 'JCaverGroup', }, - exploredCaves: { - collection: 'TCave', + exploredEntrances: { + collection: 'TEntrance', via: 'caver', - through: 'JCaverCaveExplorer', + through: 'JCaverEntranceExplorer', }, subscribedToMassifs: { diff --git a/api/models/TEntrance.js b/api/models/TEntrance.js index 4c5e2a132..5eda4f508 100644 --- a/api/models/TEntrance.js +++ b/api/models/TEntrance.js @@ -288,6 +288,12 @@ module.exports = { via: 'entrance', }, + explorerCavers: { + collection: 'TCaver', + via: 'entrance', + through: 'JCaverEntranceExplorer', + }, + redirectTo: { type: 'number', allowNull: true, diff --git a/api/services/CaverService.js b/api/services/CaverService.js index 687354039..c6538855a 100644 --- a/api/services/CaverService.js +++ b/api/services/CaverService.js @@ -65,7 +65,7 @@ module.exports = { limit: 10, sort: [{ dateInscription: 'DESC' }], }) - .populate('exploredCaves') + .populate('exploredEntrances') .populate('grottos') .populate('groups') .populate('subscribedToCountries') @@ -82,25 +82,6 @@ module.exports = { ? 'CAVER' : 'AUTHOR'; - // Set entrances for explored caves - // eslint-disable-next-line global-require - const CaveService = require('./CaveService'); - await CaveService.setEntrances(caver.exploredCaves); - - await NameService.setNames([...caver.exploredCaves], 'cave'); - - // Split caves between entrances and networks (cave) - caver.exploredNetworks = []; - caver.exploredEntrances = []; - for (const cave of caver.exploredCaves) { - if (cave.entrances.length > 1) { - caver.exploredNetworks.push(cave); - } - if (cave.entrances.length === 1) { - caver.exploredEntrances.push(cave.entrances.pop()); - } - } - await Promise.all([ NameService.setNames(caver.exploredEntrances, 'entrance'), NameService.setNames(caver.grottos, 'grotto'), @@ -117,9 +98,7 @@ module.exports = { language: caver.language, groups: caver.groups, grottos: caver.grottos, - exploredCaves: caver.exploredCaves, exploredEntrances: caver.exploredEntrances, - exploredNetworks: caver.exploredNetworks, documents: caver.documents, subscribedToMassifs: caver.subscribedToMassifs, subscribedToCountries: caver.subscribedToCountries, diff --git a/api/services/mapping/converters.js b/api/services/mapping/converters.js index 4e8a9b4f7..2e8e867bb 100644 --- a/api/services/mapping/converters.js +++ b/api/services/mapping/converters.js @@ -116,7 +116,6 @@ const c = { // Convert collections listParser('exploredEntrances', c.toSimpleEntrance); - listParser('exploredNetworks', c.toSimpleCave); listParser('groups', (group) => group); listParser('documents', c.toSimpleDocument); listParser('grottos', c.toSimpleOrganization, 'organizations'); diff --git a/assets/swaggerV1.yaml b/assets/swaggerV1.yaml index 1d7b8d2ee..7ff9ade5d 100644 --- a/assets/swaggerV1.yaml +++ b/assets/swaggerV1.yaml @@ -928,62 +928,64 @@ paths: '403': description: You are not authorized to remove a caver from a group. - '/caves/{caveId}/cavers/{caverId}': + '/entrances/{entranceId}/cavers/{caverId}': put: tags: - - caves + - entrances - cavers - description: Add a caver as a cave explorer. Users can only add caves to their own profile. Moderators and administrators can add caves to any user's profile. + description: Add a caver as an entrance explorer. Users can only add entrances to their own profile. Moderators and administrators can add entrances to any user's profile. parameters: - - name: caveId + - name: entranceId in: path - description: Cave id to add the explorer to. + description: Entrance id to add the explorer to. required: true schema: type: string - name: caverId in: path - description: Caver id to be added as cave explorer. + description: Caver id to be added as entrance explorer. required: true schema: type: string responses: '204': description: Successful operation - '400': - description: Cave not found or caver is already exploring this cave. '401': description: Authentication required. '403': - description: You can only manage your own explored caves (unless you are a moderator or administrator). + description: You can only manage your own explored entrances (unless you are a moderator or administrator). + '404': + description: Entrance or caver not found. + '409': + description: Caver is already registered as an explorer of this entrance. delete: tags: - - caves + - entrances - cavers - description: Remove a caver as a cave explorer. Users can only remove caves from their own profile. Moderators and administrators can remove caves from any user's profile. + description: Remove a caver as an entrance explorer. Users can only remove entrances from their own profile. Moderators and administrators can remove entrances from any user's profile. parameters: - - name: caveId + - name: entranceId in: path - description: Cave id to remove the explorer from. + description: Entrance id to remove the explorer from. required: true schema: type: string - name: caverId in: path - description: Caver id to be removed as cave explorer. + description: Caver id to be removed as entrance explorer. required: true schema: type: string responses: '204': description: Successful operation - '400': - description: Cave not found or caver is not exploring this cave. '401': description: Authentication required. '403': - description: You can only manage your own explored caves (unless you are a moderator or administrator). + description: You can only manage your own explored entrances (unless you are a moderator or administrator). + '404': + description: Entrance, caver, or explorer relationship not found. '/cavers/{caverId}/organizations/{organizationId}': put: @@ -7515,10 +7517,6 @@ components: type: array items: $ref: '#/components/schemas/Entrance' - exploredNetworks: - type: array - items: - $ref: '#/components/schemas/Cave' groups: type: array items: diff --git a/config/policies.js b/config/policies.js index c1b0200a4..0a8b2a2b8 100644 --- a/config/policies.js +++ b/config/policies.js @@ -65,7 +65,7 @@ module.exports.policies = { 'v1/caver/users-count': true, 'v1/caver/find': ['validateId'], 'v1/caver/get-subscriptions': ['validateId'], - 'v1/caver/add-explored-cave': 'tokenAuth', + 'v1/caver/add-explored-entrance': 'tokenAuth', 'v1/caver/create': 'tokenAuth', 'v1/caver/get-groups': 'tokenAuth', 'v1/caver/get-admins': 'tokenAuth', @@ -74,7 +74,7 @@ module.exports.policies = { 'v1/caver/get-contributors': 'tokenAuth', 'v1/caver/get-users': 'tokenAuth', 'v1/caver/put-on-group': 'tokenAuth', - 'v1/caver/remove-explored-cave': 'tokenAuth', + 'v1/caver/remove-explored-entrance': 'tokenAuth', 'v1/caver/ban': 'tokenAuth', 'v1/caver/unban': 'tokenAuth', 'v1/caver/remove-from-group': 'tokenAuth', diff --git a/config/routes.js b/config/routes.js index 402af16e5..4af11d567 100644 --- a/config/routes.js +++ b/config/routes.js @@ -144,6 +144,10 @@ module.exports.routes = { 'v1/entrance/add-document', 'DELETE /api/v1/entrances/:id': 'v1/entrance/delete', 'POST /api/v1/entrances/:id/restore': 'v1/entrance/restore', + 'PUT /api/v1/entrances/:entranceId/cavers/:caverId': + 'v1/caver/add-explored-entrance', + 'DELETE /api/v1/entrances/:entranceId/cavers/:caverId': + 'v1/caver/remove-explored-entrance', // Notification 'GET /api/v1/notifications/unread/count': 'v1/notification/count-unread', @@ -166,9 +170,6 @@ module.exports.routes = { 'v1/organization/add-explored-cave', 'DELETE /api/v1/caves/:caveId/organizations/:organizationId': 'v1/organization/remove-explored-cave', - 'PUT /api/v1/caves/:caveId/cavers/:caverId': 'v1/caver/add-explored-cave', - 'DELETE /api/v1/caves/:caveId/cavers/:caverId': - 'v1/caver/remove-explored-cave', /** * @deprecated use api/v1/caves instead */ diff --git a/sql/0_tables.sql b/sql/0_tables.sql index 0963dafe5..79e1a18a2 100644 --- a/sql/0_tables.sql +++ b/sql/0_tables.sql @@ -1469,3 +1469,15 @@ CREATE TABLE j_guideline_massif ( CREATE INDEX idx_j_guideline_massif_guideline ON j_guideline_massif(id_guideline); CREATE INDEX idx_j_guideline_massif_massif ON j_guideline_massif(id_massif); + +-- j_caver_entrance_explorer definition +CREATE TABLE IF NOT EXISTS j_caver_entrance_explorer ( + id_caver int4 NOT NULL, + id_entrance int4 NOT NULL, + CONSTRAINT j_caver_entrance_explorer_pk PRIMARY KEY (id_caver, id_entrance), + CONSTRAINT j_caver_entrance_explorer_t_caver_fk FOREIGN KEY (id_caver) REFERENCES public.t_caver(id), + CONSTRAINT j_caver_entrance_explorer_t_entrance_fk FOREIGN KEY (id_entrance) REFERENCES public.t_entrance(id) +); + +CREATE INDEX IF NOT EXISTS j_caver_entrance_explorer_id_caver_idx ON j_caver_entrance_explorer (id_caver); +CREATE INDEX IF NOT EXISTS j_caver_entrance_explorer_id_entrance_idx ON j_caver_entrance_explorer (id_entrance); diff --git a/sql/9_13_2026_06_23_caver_entrance_explorer.sql b/sql/9_13_2026_06_23_caver_entrance_explorer.sql new file mode 100644 index 000000000..9bb0b8f3e --- /dev/null +++ b/sql/9_13_2026_06_23_caver_entrance_explorer.sql @@ -0,0 +1,12 @@ +\c grottoce; + +CREATE TABLE IF NOT EXISTS j_caver_entrance_explorer ( + id_caver int4 NOT NULL, + id_entrance int4 NOT NULL, + CONSTRAINT j_caver_entrance_explorer_pk PRIMARY KEY (id_caver, id_entrance), + CONSTRAINT j_caver_entrance_explorer_t_caver_fk FOREIGN KEY (id_caver) REFERENCES public.t_caver(id), + CONSTRAINT j_caver_entrance_explorer_t_entrance_fk FOREIGN KEY (id_entrance) REFERENCES public.t_entrance(id) +); + +CREATE INDEX IF NOT EXISTS j_caver_entrance_explorer_id_caver_idx ON j_caver_entrance_explorer (id_caver); +CREATE INDEX IF NOT EXISTS j_caver_entrance_explorer_id_entrance_idx ON j_caver_entrance_explorer (id_entrance); diff --git a/sql/9_14_2026_06_23_mock_caver_entrance_explorer.sql b/sql/9_14_2026_06_23_mock_caver_entrance_explorer.sql new file mode 100644 index 000000000..a3c8a219b --- /dev/null +++ b/sql/9_14_2026_06_23_mock_caver_entrance_explorer.sql @@ -0,0 +1,34 @@ +\c grottoce; + +-- Mock data for j_caver_entrance_explorer +-- Derived from j_caver_cave_explorer by expanding to all entrances per cave + +INSERT INTO public.j_caver_entrance_explorer (id_caver, id_entrance) +VALUES + (1, 1), -- cave 75070, entrance 1 + (1, 7), -- cave 7, entrance 7 + (1, 13), -- cave 13, entrance 13 + (1, 14), -- cave 14, entrance 14 + (2, 3), -- cave 75084, entrance 3 + (2, 9), -- cave 75277, entrance 9 + (2, 10), -- cave 75072, entrance 10 + (2, 11), -- cave 75073, entrance 11 + (2, 12), -- cave 75363, entrance 12 + (2, 13), -- cave 13, entrance 13 + (2, 14), -- cave 14, entrance 14 + (2, 15), -- cave 15, entrance 15 + (2, 16), -- cave 75074, entrance 16 + (4, 1), -- cave 75070, entrance 1 + (4, 2), -- cave 75142, entrance 2 + (5, 1), -- cave 75070, entrance 1 + (5, 2), -- cave 75142, entrance 2 + (5, 4), -- cave 75071, entrance 4 + (5, 5), -- cave 5, entrance 5 + (5, 6), -- cave 6, entrance 6 + (5, 12), -- cave 75363, entrance 12 + (5, 13), -- cave 13, entrance 13 + (5, 15), -- cave 15, entrance 15 + (6, 1), -- cave 75070, entrance 1 + (6, 5), -- cave 5, entrance 5 + (6, 7) -- cave 7, entrance 7 +ON CONFLICT (id_caver, id_entrance) DO NOTHING; diff --git a/sql/9_15_2026_06_23_migrate_cave_explorer_to_entrance.sql b/sql/9_15_2026_06_23_migrate_cave_explorer_to_entrance.sql new file mode 100644 index 000000000..3280aa2bd --- /dev/null +++ b/sql/9_15_2026_06_23_migrate_cave_explorer_to_entrance.sql @@ -0,0 +1,12 @@ +\c grottoce; + +-- Migrate all caver-cave explorer relationships to caver-entrance. +-- For each caver-cave link, one row is created per entrance of that cave. +-- For multi-entrance caves (networks), the caver is linked to every entrance. +-- Users can remove incorrect relationships via the DELETE endpoint. + +INSERT INTO j_caver_entrance_explorer (id_caver, id_entrance) +SELECT jcce.id_caver, te.id +FROM j_caver_cave_explorer jcce +JOIN t_entrance te ON te.id_cave = jcce.id_cave +ON CONFLICT (id_caver, id_entrance) DO NOTHING; diff --git a/test/customSQL.js b/test/customSQL.js index e861cf532..aa0a91fb7 100644 --- a/test/customSQL.js +++ b/test/customSQL.js @@ -91,6 +91,9 @@ CREATE INDEX IF NOT EXISTS idx_t_rigging_entrance ON t_rigging(id_entrance) WHER CREATE INDEX IF NOT EXISTS idx_t_rigging_cave ON t_rigging(id_cave) WHERE id_cave IS NOT NULL; CREATE INDEX IF NOT EXISTS j_caver_cave_explorer_id_caver_idx ON j_caver_cave_explorer (id_caver); CREATE INDEX IF NOT EXISTS j_caver_cave_explorer_id_cave_idx ON j_caver_cave_explorer (id_cave); +CREATE INDEX IF NOT EXISTS j_caver_entrance_explorer_id_caver_idx ON j_caver_entrance_explorer (id_caver); +CREATE INDEX IF NOT EXISTS j_caver_entrance_explorer_id_entrance_idx ON j_caver_entrance_explorer (id_entrance); +CREATE UNIQUE INDEX IF NOT EXISTS j_caver_entrance_explorer_unique ON j_caver_entrance_explorer (id_entrance, id_caver); CREATE INDEX IF NOT EXISTS idx_document_id_parent ON t_document(id_parent) WHERE id_parent IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_document_hierarchy ON t_document(id, id_parent); CREATE INDEX IF NOT EXISTS idx_t_document_type ON t_document(id_type); diff --git a/test/fixtureOrder.js b/test/fixtureOrder.js index 9510a72e7..2a71ea758 100644 --- a/test/fixtureOrder.js +++ b/test/fixtureOrder.js @@ -29,6 +29,7 @@ module.exports = [ 'tcave', 'tentrance', 'jcavercaveexplorer', + 'jcaverentranceexplorer', 'jcaverregionsubscription', 'tmassif', 'thistory', diff --git a/test/fixtures/jcaverentranceexplorer.json b/test/fixtures/jcaverentranceexplorer.json new file mode 100644 index 000000000..8f87bb97e --- /dev/null +++ b/test/fixtures/jcaverentranceexplorer.json @@ -0,0 +1,22 @@ +[ + { + "caver": 6, + "entrance": 4 + }, + { + "caver": 6, + "entrance": 5 + }, + { + "caver": 102, + "entrance": 4 + }, + { + "caver": 102, + "entrance": 5 + }, + { + "caver": 103, + "entrance": 6 + } +] diff --git a/test/integration/0_models/JCaverEntranceExplorer.test.js b/test/integration/0_models/JCaverEntranceExplorer.test.js new file mode 100644 index 000000000..b1a1f4b7c --- /dev/null +++ b/test/integration/0_models/JCaverEntranceExplorer.test.js @@ -0,0 +1,123 @@ +const should = require('should'); + +describe('JCaverEntranceExplorer model associations', () => { + let testCaver; + let testCave; + let testEntrance; + + before(async () => { + testCaver = await TCaver.create({ + dateInscription: new Date(), + mail: `test-explorer-${Date.now()}@test.com`, + nickname: 'Explorer Test', + language: '000', + }).fetch(); + + testCave = await TCave.create({ + author: 1, + dateInscription: new Date(), + dateReviewed: new Date(), + }).fetch(); + + testEntrance = await TEntrance.create({ + author: 1, + dateInscription: new Date(), + latitude: '45.5', + longitude: '6.5', + cave: testCave.id, + geology: 'Q35758', + }).fetch(); + }); + + after(async () => { + // Clear any lingering junction rows first (idempotent — safe if already gone) + if (testCaver && testEntrance) { + await TCaver.removeFromCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + } + if (testEntrance) await TEntrance.destroyOne({ id: testEntrance.id }); + if (testCave) await TCave.destroyOne({ id: testCave.id }); + if (testCaver) await TCaver.destroyOne({ id: testCaver.id }); + }); + + describe('TCaver.populate(exploredEntrances)', () => { + before(async () => { + await TCaver.addToCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + }); + + after(async () => { + await TCaver.removeFromCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + }); + + it('should return entrance objects when populated', async () => { + const caver = await TCaver.findOne(testCaver.id).populate( + 'exploredEntrances' + ); + should(caver.exploredEntrances).be.an.Array(); + should(caver.exploredEntrances).have.length(1); + should(caver.exploredEntrances[0].id).equal(testEntrance.id); + }); + }); + + describe('TEntrance.populate(explorerCavers)', () => { + before(async () => { + await TCaver.addToCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + }); + + after(async () => { + await TCaver.removeFromCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + }); + + it('should return caver objects when populated', async () => { + const entrance = await TEntrance.findOne(testEntrance.id).populate( + 'explorerCavers' + ); + should(entrance.explorerCavers).be.an.Array(); + should(entrance.explorerCavers).have.length(1); + should(entrance.explorerCavers[0].id).equal(testCaver.id); + }); + }); + + describe('Collection add/remove', () => { + it('should add and remove via Waterline collection methods', async () => { + await TCaver.addToCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + + let caver = await TCaver.findOne(testCaver.id).populate( + 'exploredEntrances' + ); + should(caver.exploredEntrances).have.length(1); + + await TCaver.removeFromCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + + caver = await TCaver.findOne(testCaver.id).populate('exploredEntrances'); + should(caver.exploredEntrances).have.length(0); + }); + }); +}); diff --git a/test/integration/1_services/CaverExploredCaves.test.js b/test/integration/1_services/CaverExploredCaves.test.js deleted file mode 100644 index fd0ffe5d9..000000000 --- a/test/integration/1_services/CaverExploredCaves.test.js +++ /dev/null @@ -1,143 +0,0 @@ -const should = require('should'); - -describe('Caver explored caves relationship', () => { - let testCaver; - let testCave1; // Single entrance cave - let testCave2; // Multi-entrance cave (network) - let testEntrance1; - let testEntrance2; - let testEntrance3; - - before(async () => { - // Create test caver - testCaver = await TCaver.create({ - dateInscription: new Date(), - mail: `test-${Date.now()}@test.com`, - nickname: 'Test Caver', - language: '000', - }).fetch(); - - // Create cave with single entrance - testCave1 = await TCave.create({ - author: 1, - dateInscription: new Date(), - dateReviewed: new Date(), - }).fetch(); - - testEntrance1 = await TEntrance.create({ - author: 1, - dateInscription: new Date(), - latitude: '45.5', - longitude: '6.5', - cave: testCave1.id, - geology: 'Q35758', - }).fetch(); - - // Create cave with multiple entrances (network) - testCave2 = await TCave.create({ - author: 1, - dateInscription: new Date(), - dateReviewed: new Date(), - }).fetch(); - - testEntrance2 = await TEntrance.create({ - author: 1, - dateInscription: new Date(), - latitude: '45.6', - longitude: '6.6', - cave: testCave2.id, - geology: 'Q35758', - }).fetch(); - - testEntrance3 = await TEntrance.create({ - author: 1, - dateInscription: new Date(), - latitude: '45.7', - longitude: '6.7', - cave: testCave2.id, - geology: 'Q35758', - }).fetch(); - }); - - after(async () => { - // Cleanup - if (testEntrance1) await TEntrance.destroyOne({ id: testEntrance1.id }); - if (testEntrance2) await TEntrance.destroyOne({ id: testEntrance2.id }); - if (testEntrance3) await TEntrance.destroyOne({ id: testEntrance3.id }); - if (testCave1) await TCave.destroyOne({ id: testCave1.id }); - if (testCave2) await TCave.destroyOne({ id: testCave2.id }); - if (testCaver) await TCaver.destroyOne({ id: testCaver.id }); - }); - - describe('Junction table j_caver_cave_explorer', () => { - it('should link caver to cave', async () => { - await TCaver.addToCollection(testCaver.id, 'exploredCaves', testCave1.id); - - const caver = await TCaver.findOne(testCaver.id).populate( - 'exploredCaves' - ); - should(caver.exploredCaves).have.length(1); - should(caver.exploredCaves[0].id).equal(testCave1.id); - }); - - it('should remove caver-cave link', async () => { - await TCaver.removeFromCollection( - testCaver.id, - 'exploredCaves', - testCave1.id - ); - - const caver = await TCaver.findOne(testCaver.id).populate( - 'exploredCaves' - ); - should(caver.exploredCaves).have.length(0); - }); - }); - - describe('CaverService.getCaver()', () => { - before(async () => { - // Add both caves to the caver - await TCaver.addToCollection(testCaver.id, 'exploredCaves', [ - testCave1.id, - testCave2.id, - ]); - }); - - it('should split caves into exploredEntrances and exploredNetworks', async () => { - // eslint-disable-next-line global-require - const CaverService = require('../../../api/services/CaverService'); - const caver = await CaverService.getCaver(testCaver.id); - - should(caver).have.property('exploredEntrances'); - should(caver).have.property('exploredNetworks'); - should(caver).have.property('exploredCaves'); - - // exploredCaves contains the raw populated array - should(caver.exploredCaves).have.length(2); - - // Cave with 1 entrance should be in exploredEntrances - should(caver.exploredEntrances).have.length(1); - should(caver.exploredEntrances[0].id).equal(testEntrance1.id); - - // Cave with 2+ entrances should be in exploredNetworks - should(caver.exploredNetworks).have.length(1); - should(caver.exploredNetworks[0].id).equal(testCave2.id); - should(caver.exploredNetworks[0].entrances).have.length(2); - }); - - it('should set names for entrances and networks', async () => { - // eslint-disable-next-line global-require - const CaverService = require('../../../api/services/CaverService'); - const caver = await CaverService.getCaver(testCaver.id); - - // Check that names are set (even if empty array) - caver.exploredEntrances.forEach((entrance) => { - should(entrance).have.property('names'); - }); - - caver.exploredNetworks.forEach((network) => { - should(network).have.property('names'); - }); - }); - }); -}); diff --git a/test/integration/1_services/CaverExploredEntrances.test.js b/test/integration/1_services/CaverExploredEntrances.test.js new file mode 100644 index 000000000..9d69c3fe0 --- /dev/null +++ b/test/integration/1_services/CaverExploredEntrances.test.js @@ -0,0 +1,81 @@ +const should = require('should'); + +describe('Caver explored entrances relationship', () => { + let testCaver; + let testCave; + let testEntrance; + + before(async () => { + testCaver = await TCaver.create({ + dateInscription: new Date(), + mail: `test-svc-${Date.now()}@test.com`, + nickname: 'Test Service Caver', + language: '000', + }).fetch(); + + testCave = await TCave.create({ + author: 1, + dateInscription: new Date(), + dateReviewed: new Date(), + }).fetch(); + + testEntrance = await TEntrance.create({ + author: 1, + dateInscription: new Date(), + latitude: '45.5', + longitude: '6.5', + cave: testCave.id, + geology: 'Q35758', + }).fetch(); + + // Link caver to entrance + await TCaver.addToCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + }); + + after(async () => { + await TCaver.removeFromCollection( + testCaver.id, + 'exploredEntrances', + testEntrance.id + ); + if (testEntrance) await TEntrance.destroyOne({ id: testEntrance.id }); + if (testCave) await TCave.destroyOne({ id: testCave.id }); + if (testCaver) await TCaver.destroyOne({ id: testCaver.id }); + }); + + describe('CaverService.getCaver()', () => { + it('should return exploredEntrances from junction table', async () => { + // eslint-disable-next-line global-require + const CaverService = require('../../../api/services/CaverService'); + const caver = await CaverService.getCaver(testCaver.id); + + should(caver).have.property('exploredEntrances'); + should(caver.exploredEntrances).be.an.Array(); + should(caver.exploredEntrances).have.length(1); + should(caver.exploredEntrances[0].id).equal(testEntrance.id); + }); + + it('should NOT have exploredCaves or exploredNetworks properties', async () => { + // eslint-disable-next-line global-require + const CaverService = require('../../../api/services/CaverService'); + const caver = await CaverService.getCaver(testCaver.id); + + should(caver).not.have.property('exploredCaves'); + should(caver).not.have.property('exploredNetworks'); + }); + + it('should set names on explored entrances', async () => { + // eslint-disable-next-line global-require + const CaverService = require('../../../api/services/CaverService'); + const caver = await CaverService.getCaver(testCaver.id); + + caver.exploredEntrances.forEach((entrance) => { + should(entrance).have.property('names'); + }); + }); + }); +}); diff --git a/test/integration/1_services/CaverService.test.js b/test/integration/1_services/CaverService.test.js index 4efe73e84..d8450cb2a 100644 --- a/test/integration/1_services/CaverService.test.js +++ b/test/integration/1_services/CaverService.test.js @@ -83,12 +83,10 @@ describe('CaverService', () => { should(caver.grottos.length).equal(2); should(caver.grottos).containDeep([{ id: 1 }, { id: 2 }]); should(caver.groups).containDeep([{ id: 1 }]); - should(caver.exploredNetworks.length).equal(1); - should(caver.exploredNetworks[0].entrances.length).equal(2); - should(caver.exploredNetworks[0].entrances).containDeep([ - { id: 4 }, - { id: 5 }, - ]); + should(caver.exploredEntrances.length).equal(2); + should(caver.exploredEntrances).containDeep([{ id: 4 }, { id: 5 }]); + should.not.exist(caver.exploredCaves); + should.not.exist(caver.exploredNetworks); should(caver.language).equal('fra'); should.not.exist(caver.password); should.not.exist(caver.activationCode); diff --git a/test/integration/4_routes/Cavers/delete.test.js b/test/integration/4_routes/Cavers/delete.test.js index 0068cca96..328d4580f 100644 --- a/test/integration/4_routes/Cavers/delete.test.js +++ b/test/integration/4_routes/Cavers/delete.test.js @@ -77,21 +77,21 @@ describe('Caver features', () => { it('should merge source caver data into target and delete source', async () => { // Verify source caver exists and has data before merge const sourceBefore = await TCaver.findOne(102) - .populate('exploredCaves') + .populate('exploredEntrances') .populate('documents') .populate('subscribedToRegions'); should(sourceBefore).not.be.null(); - should(sourceBefore.exploredCaves).have.length(1); + should(sourceBefore.exploredEntrances).have.length(2); should(sourceBefore.documents).have.length(1); should(sourceBefore.subscribedToRegions).have.length(1); // Verify target caver before merge const targetBefore = await TCaver.findOne(103) - .populate('exploredCaves') + .populate('exploredEntrances') .populate('documents') .populate('subscribedToRegions'); should(targetBefore).not.be.null(); - should(targetBefore.exploredCaves).have.length(1); + should(targetBefore.exploredEntrances).have.length(1); should(targetBefore.documents).have.length(1); should(targetBefore.subscribedToRegions).have.length(0); @@ -111,14 +111,16 @@ describe('Caver features', () => { // Verify target caver received the merged data const targetAfter = await TCaver.findOne(103) - .populate('exploredCaves') + .populate('exploredEntrances') .populate('documents') .populate('subscribedToRegions'); should(targetAfter).not.be.null(); - // Target should have its own cave (4) + source's cave (3) - should(targetAfter.exploredCaves).have.length(2); - const exploredCaveIds = targetAfter.exploredCaves.map((c) => c.id); - should(exploredCaveIds).containDeep([3, 4]); + // Target should have its own entrance (6) + source's entrances (4, 5) + should(targetAfter.exploredEntrances).have.length(3); + const exploredEntranceIds = targetAfter.exploredEntrances.map( + (e) => e.id + ); + should(exploredEntranceIds).containDeep([4, 5, 6]); // Target should have its own document (4) + source's document (3) should(targetAfter.documents).have.length(2); const documentIds = targetAfter.documents.map((d) => d.id); diff --git a/test/integration/4_routes/Cavers/explored-entrance.test.js b/test/integration/4_routes/Cavers/explored-entrance.test.js deleted file mode 100644 index e4fc4b323..000000000 --- a/test/integration/4_routes/Cavers/explored-entrance.test.js +++ /dev/null @@ -1,105 +0,0 @@ -const supertest = require('supertest'); -const should = require('should'); -const AuthTokenService = require('../../AuthTokenService'); - -describe('Caver explored cave endpoints', () => { - let userToken; - let userId; - let testCave; - - before(async () => { - userToken = await AuthTokenService.getRawBearerUserToken(); - const tokenData = await AuthTokenService.getUserToken(); - userId = tokenData.id; - - // Create test cave - testCave = await TCave.create({ - author: userId, - dateInscription: new Date(), - dateReviewed: new Date(), - }).fetch(); - }); - - after(async () => { - // Cleanup - if (testCave) await TCave.destroyOne({ id: testCave.id }); - }); - - describe('PUT /api/v1/caves/:caveId/cavers/:caverId', () => { - it('should add cave to caver', (done) => { - supertest(sails.hooks.http.app) - .put(`/api/v1/caves/${testCave.id}/cavers/${userId}`) - .set('Authorization', userToken) - .set('Content-type', 'application/json') - .set('Accept', 'application/json') - .expect(204) - .end(async (err) => { - if (err) return done(err); - - // Verify the cave was added - const caver = await TCaver.findOne(userId).populate('exploredCaves'); - const hasCave = caver.exploredCaves.some((c) => c.id === testCave.id); - should(hasCave).be.true(); - - return done(); - }); - }); - - it('should return 404 for non-existent cave', (done) => { - supertest(sails.hooks.http.app) - .put(`/api/v1/caves/999999/cavers/${userId}`) - .set('Authorization', userToken) - .set('Content-type', 'application/json') - .set('Accept', 'application/json') - .expect(404, done); - }); - - it('should return 400 when caver already explores cave', (done) => { - supertest(sails.hooks.http.app) - .put(`/api/v1/caves/${testCave.id}/cavers/${userId}`) - .set('Authorization', userToken) - .set('Content-type', 'application/json') - .set('Accept', 'application/json') - .expect(400, done); - }); - }); - - describe('DELETE /api/v1/caves/:caveId/cavers/:caverId', () => { - it('should remove cave from caver', (done) => { - supertest(sails.hooks.http.app) - .delete(`/api/v1/caves/${testCave.id}/cavers/${userId}`) - .set('Authorization', userToken) - .set('Content-type', 'application/json') - .set('Accept', 'application/json') - .expect(204) - .end(async (err) => { - if (err) return done(err); - - // Verify the cave was removed - const caver = await TCaver.findOne(userId).populate('exploredCaves'); - const hasCave = caver.exploredCaves.some((c) => c.id === testCave.id); - should(hasCave).be.false(); - - return done(); - }); - }); - - it('should return 404 for non-existent cave', (done) => { - supertest(sails.hooks.http.app) - .delete(`/api/v1/caves/999999/cavers/${userId}`) - .set('Authorization', userToken) - .set('Content-type', 'application/json') - .set('Accept', 'application/json') - .expect(404, done); - }); - - it('should return 400 when caver does not explore cave', (done) => { - supertest(sails.hooks.http.app) - .delete(`/api/v1/caves/${testCave.id}/cavers/${userId}`) - .set('Authorization', userToken) - .set('Content-type', 'application/json') - .set('Accept', 'application/json') - .expect(400, done); - }); - }); -}); diff --git a/test/integration/4_routes/Cavers/exploredCaves.test.js b/test/integration/4_routes/Cavers/exploredCaves.test.js deleted file mode 100644 index db4fa9e5d..000000000 --- a/test/integration/4_routes/Cavers/exploredCaves.test.js +++ /dev/null @@ -1,121 +0,0 @@ -const supertest = require('supertest'); -const should = require('should'); -const AuthTokenService = require('../../AuthTokenService'); - -describe('Caver explored caves features', () => { - let userToken; - let userId; - let moderatorId; - let testCave; - - before(async () => { - userToken = await AuthTokenService.getRawBearerUserToken(); - const tokenData = await AuthTokenService.getUserToken(); - userId = tokenData.id; - - const moderatorTokenData = await AuthTokenService.getModeratorToken(); - moderatorId = moderatorTokenData.id; - - testCave = await TCave.create({ - author: userId, - dateInscription: new Date(), - dateReviewed: new Date(), - }).fetch(); - }); - - after(async () => { - if (testCave) await TCave.destroyOne({ id: testCave.id }); - }); - - describe('PUT /api/v1/caves/:caveId/cavers/:caverId', () => { - it('should return 401 when not authenticated', (done) => { - supertest(sails.hooks.http.app) - .put(`/api/v1/caves/${testCave.id}/cavers/${userId}`) - .expect(401, done); - }); - - it('should return 403 when trying to add explored cave to another user', (done) => { - supertest(sails.hooks.http.app) - .put(`/api/v1/caves/${testCave.id}/cavers/${moderatorId}`) - .set('Authorization', userToken) - .expect(403, done); - }); - - it('should return 404 on non-existing cave', (done) => { - supertest(sails.hooks.http.app) - .put(`/api/v1/caves/987654321/cavers/${userId}`) - .set('Authorization', userToken) - .expect(404, done); - }); - - it('should return 204 and add explored cave to own profile', async () => { - const initialCaver = - await TCaver.findOne(userId).populate('exploredCaves'); - const initialCount = initialCaver.exploredCaves.length; - - await supertest(sails.hooks.http.app) - .put(`/api/v1/caves/${testCave.id}/cavers/${userId}`) - .set('Authorization', userToken) - .expect(204); - - const updatedCaver = - await TCaver.findOne(userId).populate('exploredCaves'); - should(updatedCaver.exploredCaves.length).be.greaterThan(initialCount); - }); - - it('should return 204 when moderator adds explored cave to another user', async () => { - const moderatorToken = - await AuthTokenService.getRawBearerModeratorToken(); - await supertest(sails.hooks.http.app) - .put(`/api/v1/caves/${testCave.id}/cavers/${moderatorId}`) - .set('Authorization', moderatorToken) - .expect(204); - }); - }); - - describe('DELETE /api/v1/caves/:caveId/cavers/:caverId', () => { - it('should return 401 when not authenticated', (done) => { - supertest(sails.hooks.http.app) - .delete(`/api/v1/caves/${testCave.id}/cavers/${userId}`) - .expect(401, done); - }); - - it('should return 403 when trying to remove explored cave from another user', (done) => { - supertest(sails.hooks.http.app) - .delete(`/api/v1/caves/${testCave.id}/cavers/${moderatorId}`) - .set('Authorization', userToken) - .expect(403, done); - }); - - it('should return 404 on non-existing cave', (done) => { - supertest(sails.hooks.http.app) - .delete(`/api/v1/caves/987654321/cavers/${userId}`) - .set('Authorization', userToken) - .expect(404, done); - }); - - it('should return 204 and remove explored cave', async () => { - const initialCaver = - await TCaver.findOne(userId).populate('exploredCaves'); - const initialCount = initialCaver.exploredCaves.length; - - await supertest(sails.hooks.http.app) - .delete(`/api/v1/caves/${testCave.id}/cavers/${userId}`) - .set('Authorization', userToken) - .expect(204); - - const updatedCaver = - await TCaver.findOne(userId).populate('exploredCaves'); - should(updatedCaver.exploredCaves.length).be.lessThan(initialCount); - }); - - it('should return 204 when moderator removes explored cave from another user', async () => { - const moderatorToken = - await AuthTokenService.getRawBearerModeratorToken(); - await supertest(sails.hooks.http.app) - .delete(`/api/v1/caves/${testCave.id}/cavers/${moderatorId}`) - .set('Authorization', moderatorToken) - .expect(204); - }); - }); -}); diff --git a/test/integration/4_routes/Cavers/exploredEntrances.test.js b/test/integration/4_routes/Cavers/exploredEntrances.test.js new file mode 100644 index 000000000..844799564 --- /dev/null +++ b/test/integration/4_routes/Cavers/exploredEntrances.test.js @@ -0,0 +1,198 @@ +const supertest = require('supertest'); +const should = require('should'); +const AuthTokenService = require('../../AuthTokenService'); + +describe('Caver explored entrances features', () => { + let userToken; + let userId; + let moderatorToken; + let moderatorId; + let testCave; + let testEntrance; + + before(async () => { + userToken = await AuthTokenService.getRawBearerUserToken(); + const tokenData = await AuthTokenService.getUserToken(); + userId = tokenData.id; + + moderatorToken = await AuthTokenService.getRawBearerModeratorToken(); + const moderatorTokenData = await AuthTokenService.getModeratorToken(); + moderatorId = moderatorTokenData.id; + + testCave = await TCave.create({ + author: userId, + dateInscription: new Date(), + dateReviewed: new Date(), + }).fetch(); + + testEntrance = await TEntrance.create({ + author: userId, + dateInscription: new Date(), + latitude: '45.5', + longitude: '6.5', + cave: testCave.id, + geology: 'Q35758', + }).fetch(); + }); + + after(async () => { + // Clean up junction rows first (FK constraint) + await sails.sendNativeQuery( + 'DELETE FROM j_caver_entrance_explorer WHERE id_entrance = $1', + [testEntrance.id] + ); + if (testEntrance) await TEntrance.destroyOne({ id: testEntrance.id }); + if (testCave) await TCave.destroyOne({ id: testCave.id }); + }); + + describe('PUT /api/v1/entrances/:entranceId/cavers/:caverId', () => { + afterEach(async () => { + // Clean up any relationship created during tests + await sails.sendNativeQuery( + 'DELETE FROM j_caver_entrance_explorer WHERE id_entrance = $1', + [testEntrance.id] + ); + }); + + it('should return 401 when not authenticated', (done) => { + supertest(sails.hooks.http.app) + .put(`/api/v1/entrances/${testEntrance.id}/cavers/${userId}`) + .expect(401, done); + }); + + it('should return 403 when trying to add explored entrance to another user', (done) => { + supertest(sails.hooks.http.app) + .put(`/api/v1/entrances/${testEntrance.id}/cavers/${moderatorId}`) + .set('Authorization', userToken) + .expect(403, done); + }); + + it('should return 404 on non-existing entrance', (done) => { + supertest(sails.hooks.http.app) + .put(`/api/v1/entrances/987654321/cavers/${userId}`) + .set('Authorization', userToken) + .expect(404, done); + }); + + it('should return 404 on non-existing caver', (done) => { + supertest(sails.hooks.http.app) + .put(`/api/v1/entrances/${testEntrance.id}/cavers/987654321`) + .set('Authorization', moderatorToken) + .expect(404, done); + }); + + it('should return 204 and add explored entrance to own profile', async () => { + await supertest(sails.hooks.http.app) + .put(`/api/v1/entrances/${testEntrance.id}/cavers/${userId}`) + .set('Authorization', userToken) + .expect(204); + + const caver = await TCaver.findOne(userId).populate('exploredEntrances'); + const found = caver.exploredEntrances.some( + (e) => e.id === testEntrance.id + ); + should(found).be.true(); + }); + + it('should return 204 when moderator adds explored entrance to another user', async () => { + await supertest(sails.hooks.http.app) + .put(`/api/v1/entrances/${testEntrance.id}/cavers/${moderatorId}`) + .set('Authorization', moderatorToken) + .expect(204); + }); + + it('should return 409 when relationship already exists', async () => { + // Create the relationship first + await sails.sendNativeQuery( + 'INSERT INTO j_caver_entrance_explorer (id_entrance, id_caver) VALUES ($1, $2)', + [testEntrance.id, userId] + ); + + await supertest(sails.hooks.http.app) + .put(`/api/v1/entrances/${testEntrance.id}/cavers/${userId}`) + .set('Authorization', userToken) + .expect(409); + }); + }); + + describe('DELETE /api/v1/entrances/:entranceId/cavers/:caverId', () => { + beforeEach(async () => { + // Ensure relationship exists for delete tests + await sails.sendNativeQuery( + 'INSERT INTO j_caver_entrance_explorer (id_entrance, id_caver) VALUES ($1, $2) ON CONFLICT DO NOTHING', + [testEntrance.id, userId] + ); + await sails.sendNativeQuery( + 'INSERT INTO j_caver_entrance_explorer (id_entrance, id_caver) VALUES ($1, $2) ON CONFLICT DO NOTHING', + [testEntrance.id, moderatorId] + ); + }); + + afterEach(async () => { + await sails.sendNativeQuery( + 'DELETE FROM j_caver_entrance_explorer WHERE id_entrance = $1', + [testEntrance.id] + ); + }); + + it('should return 401 when not authenticated', (done) => { + supertest(sails.hooks.http.app) + .delete(`/api/v1/entrances/${testEntrance.id}/cavers/${userId}`) + .expect(401, done); + }); + + it('should return 403 when trying to remove explored entrance from another user', (done) => { + supertest(sails.hooks.http.app) + .delete(`/api/v1/entrances/${testEntrance.id}/cavers/${moderatorId}`) + .set('Authorization', userToken) + .expect(403, done); + }); + + it('should return 404 on non-existing entrance', (done) => { + supertest(sails.hooks.http.app) + .delete(`/api/v1/entrances/987654321/cavers/${userId}`) + .set('Authorization', userToken) + .expect(404, done); + }); + + it('should return 404 on non-existing caver', (done) => { + supertest(sails.hooks.http.app) + .delete(`/api/v1/entrances/${testEntrance.id}/cavers/987654321`) + .set('Authorization', moderatorToken) + .expect(404, done); + }); + + it('should return 404 when relationship does not exist', async () => { + // Remove the relationship first + await sails.sendNativeQuery( + 'DELETE FROM j_caver_entrance_explorer WHERE id_entrance = $1 AND id_caver = $2', + [testEntrance.id, userId] + ); + + await supertest(sails.hooks.http.app) + .delete(`/api/v1/entrances/${testEntrance.id}/cavers/${userId}`) + .set('Authorization', userToken) + .expect(404); + }); + + it('should return 204 and remove explored entrance from own profile', async () => { + await supertest(sails.hooks.http.app) + .delete(`/api/v1/entrances/${testEntrance.id}/cavers/${userId}`) + .set('Authorization', userToken) + .expect(204); + + const caver = await TCaver.findOne(userId).populate('exploredEntrances'); + const found = caver.exploredEntrances.some( + (e) => e.id === testEntrance.id + ); + should(found).be.false(); + }); + + it('should return 204 when moderator removes explored entrance from another user', async () => { + await supertest(sails.hooks.http.app) + .delete(`/api/v1/entrances/${testEntrance.id}/cavers/${moderatorId}`) + .set('Authorization', moderatorToken) + .expect(204); + }); + }); +}); diff --git a/test/integration/4_routes/Cavers/find.test.js b/test/integration/4_routes/Cavers/find.test.js index 105488d98..c4a02efae 100644 --- a/test/integration/4_routes/Cavers/find.test.js +++ b/test/integration/4_routes/Cavers/find.test.js @@ -9,7 +9,6 @@ const CAVER_PROPERTIES = [ 'id', 'documents', 'exploredEntrances', - 'exploredNetworks', 'organizations', 'groups', 'language', @@ -86,9 +85,6 @@ describe('Caver features', () => { caver.exploredEntrances.forEach((entrance) => { should(entrance).have.properties('names'); }); - caver.exploredNetworks.forEach((network) => { - should(network).have.properties('names'); - }); caver.organizations.forEach((organization) => { should(organization).have.properties('name'); }); @@ -113,9 +109,6 @@ describe('Caver features', () => { caver.exploredEntrances.forEach((entrance) => { should(entrance).have.properties('name'); }); - caver.exploredNetworks.forEach((network) => { - should(network).have.properties('name'); - }); caver.organizations.forEach((organization) => { should(organization).have.properties('name'); }); diff --git a/test/integration/4_routes/Entrances/delete-auditability.test.js b/test/integration/4_routes/Entrances/delete-auditability.test.js index 97e63a223..38521f004 100644 --- a/test/integration/4_routes/Entrances/delete-auditability.test.js +++ b/test/integration/4_routes/Entrances/delete-auditability.test.js @@ -51,8 +51,8 @@ describe('Entrance features', () => { isOfInterest: false, }).fetch(); - // Link explorer caver to entrance (j_caver_cave_explorer via cave) - await TCave.addToCollection(cave.id, 'explorerCavers', [1]); + // Link explorer caver to entrance (j_caver_entrance_explorer) + await TEntrance.addToCollection(entrance.id, 'explorerCavers', [1]); // Link exploring organization to cave (j_grotto_cave_explorer) await TCave.addToCollection(cave.id, 'exploringGrottos', [ @@ -233,8 +233,8 @@ describe('Entrance features', () => { // Junction tables should be cleaned up const caverExplorer = await CommonService.query( - 'SELECT COUNT(*)::integer AS cnt FROM j_caver_cave_explorer WHERE id_cave = $1', - [cave.id] + 'SELECT COUNT(*)::integer AS cnt FROM j_caver_entrance_explorer WHERE id_entrance = $1', + [entrance.id] ); should(caverExplorer.rows[0].cnt).equal(0);