MissionController testing - 22/05/2026
1. Dead code et refactor
Section titled “1. Dead code et refactor”- Le
if (!$user) -> 401present dans les 12 methodes est inatteignable : le middlewareauth:apidu groupe renvoie deja 401 avant d’entrer dans le controller. - 404 morts : dans
acceptDeclineMissions,getMissionDetails,cancelMissionByWorker, lemission_idporte la regleexists:missions,id. Lefind()qui suit trouve donc toujours la mission -> le branchementif (!$mission) -> 404est inatteignable. (putInProgressest l’exception : son 404 couvre le cas “pas owner”, il est bien atteignable.) acceptDeclineMissionsfait ~200 lignes : toute la branche accept (creation assignment, MAJ broadcasts, mission_code, chat, Redis, notifications, stats) devrait etre extraite dansMissionService— commecancelMission/releaseAssignmentle sont deja.- Trou d’autorisation —
cancelMissionByOwner: la methode ne verifie PAS que l’utilisateur connecte est l’owner de la mission. N’importe quel utilisateur authentifie peut annuler n’importe quelle mission ayant un assignmentpending. A corriger. getMissionDetailsn’applique aucun scope : tout utilisateur authentifie peut consulter n’importe quelle mission.getOwnMissionsV2: le statutwaiting_feedbackapparait a la fois danstab=0(actives) ettab=1(terminees)- Comparaisons fragiles a la chaine
'null'($mission->latitude != 'null') : sentinelle texte au lieu d’un vrai null SQL. - Bug
getAcceptedMissions: le controller reassigne$mission->owner = [bloc resume], mais la relationownerest eager-loaded -> danstoArray()la relation (User complet) ecrase le tableau resume. Le bloc resume {business_name, manager_name} n’est jamais visible cote response. A corriger (renommer la cle, ou ne pas eager-loadowner). - Branche morte
updateMyMission: le garde autorise les statuts{open, pending}, mais la colonnemissions.statusest un enum sanspending— une mission ne peut jamais etrepending. Lependingduin_arrayest inatteignable. createMissionV2passe$mission->latitude/longitudebruts aAdminInstantMissionOpened(sans cast), contrairement aux autres appels du meme event qui castent en(float).
2. Tests de chaque route — approche features-first (non-unit)
Section titled “2. Tests de chaque route — approche features-first (non-unit)”Routes du groupe ['auth:api', 'banned', 'verified', 'onboarding'].
2.1 createMissionV2 — POST /api/create-mission-v2
Section titled “2.1 createMissionV2 — POST /api/create-mission-v2”Auth
- utilisateur non authentifie -> 401
Validation des inputs
looking_idmanquant / non integer / inexistant -> 422datemanquante / format invalide -> 422start_time/end_timemanquants ou pas au formatH:i-> 422rate_per_hourmanquant / non numeric / negatif -> 422requirement_idmanquant / non string / > 255 -> 422priorities> 255 -> 422enterprise_idfourni mais inexistant -> 422is_schedulenon booleen -> 422
Logique metier / effets observables
- happy path : delegue a
MissionService::createMission($user, $data)(creation mission + requirements + matching) ; broadcastAdminInstantMissionOpened; 200 avec la mission (relationenterprisechargee) - si
MissionService::createMissionjette uneInvalidArgumentException-> 422 avec le message de l’exception - les regles metier owner/enterprise sont portees par
MissionService::createMission(a tester cote service)
2.2 acceptDeclineMissions — POST /api/accept-decline-missions
Section titled “2.2 acceptDeclineMissions — POST /api/accept-decline-missions”Auth
- utilisateur non authentifie -> 401
Validation des inputs
mission_idmanquant / non integer / inexistant -> 422statusmanquant ou hors{0, 1}-> 422
Gardes metier
status=1et le worker a deja unMissionAssignmentpending-> 422 (“You already have a mission in progress or pending.”)- la mission a deja un
MissionAssignment(peu importe le worker) -> 422 (“Mission already assigned.”)
Logique metier — accept (status=1)
- cree un
MissionAssignmentpendingpour le worker connecte - les autres
MissionWorkerBroadcastpendingpassentdeclined, celui du worker passeaccepted - broadcast
RemoveMissionaux autres workers ; les broadcasts non-acceptedsont supprimes - la mission passe
status = matched+mission_codegenere (entier 1000-9999) ; broadcastAdminInstantMissionClosed - ouverture du chat (
ChatService::createChat) - cleanup Redis (
removeMissionOpen,removeWorkerOnline) + recompute tension si lat/lng valides — echec catche/logue, la requete reussit - broadcast
MissionAccepted; notification a l’owner — echecs catches/logues WorkerProfile.ready_to_work = 0; broadcastWorkerStatusChanged(statut MATCHED) ;StatsService::onMissionMatched- reponse 200 :
missionData(mission +distance_kmworker<->mission + blocowner_detail)
Logique metier — decline (status=0)
- le
MissionWorkerBroadcastdu worker passedeclined; aucun autre effet ; 200
2.3 getMissionIds — GET /api/get-mission-ids
Section titled “2.3 getMissionIds — GET /api/get-mission-ids”Auth
- utilisateur non authentifie -> 401
Garde metier
- worker sans
WorkerProfileouready_to_workfalsy -> 403 (“You must be ready to work to view missions.”)
Logique metier / effets observables
- worker
ready_to_work-> renvoie les IDs des missions matchables (MatchingService::getMatchingMissionsForWorker), tries par score decroissant - 200
2.4 getOwnMissionsV2 — GET /api/get-own-missionsV2
Section titled “2.4 getOwnMissionsV2 — GET /api/get-own-missionsV2”Auth
- utilisateur non authentifie -> 401
Logique metier / effets observables
- liste paginee (10 par page) des missions de l’owner connecte (
owner_id) tab=0(defaut) -> statutsopen/matched/in_progress/waiting_feedbacktab=1-> statutscompleted/waiting_feedback- chaque mission porte un flag
given_feedback(bool : l’owner a-t-il deja note) - triees par
iddecroissant - liste non vide -> reponse paginee (
nextpage= page + 1) ; liste vide ->data: []+ message “No Mission.”
2.5 finishOwnMission — POST /api/finish-mission
Section titled “2.5 finishOwnMission — POST /api/finish-mission”Auth
- utilisateur non authentifie -> 401
Validation des inputs
mission_idmanquant / non integer / inexistant -> 422
Gardes metier
- mission inexistante pour cet owner, OU non possedee par le user, OU
status != in_progress-> 422 (“Mission not found or not in progress”)
Logique metier / effets observables
- happy path : delegue a
MissionService::progress_ended($mission->id); 200 avecmission_id
2.6 getMissionDetails — GET /api/get-mission-details
Section titled “2.6 getMissionDetails — GET /api/get-mission-details”Auth
- utilisateur non authentifie -> 401
Validation des inputs
mission_idmanquant / non integer / inexistant -> 422
Logique metier / effets observables
- renvoie la mission + relations (
looking,owner,enterprise,feedbacks,missionAssignment) matching_time=missionAssignment.created_atou null si pas d’assignment- bloc
owner_detailsavec les cles : [first_name, last_name, business_name, manager_firstname, manager_lastname, profile_photo, address, enterprise_id] given_feedback: bool selon que le user courant a deja note- aucun scope : tout utilisateur authentifie peut consulter n’importe quelle mission (cf section 1)
2.7 getAcceptedMissions — GET /api/get-acceped-missions
Section titled “2.7 getAcceptedMissions — GET /api/get-acceped-missions”Auth
- utilisateur non authentifie -> 401
Logique metier / effets observables
- liste paginee (10 par page) des missions assignees au worker connecte (via
mission_assignments.worker_id), hors statutcancel - triees par
iddecroissant - chaque mission : flag
given_feedback - le controller tente de remplacer
ownerpar un bloc resume {first_name, last_name, business_name, manager_name}, mais la relationownereager-loaded ecrase ce tableau (cf bug section 1) : la response renvoie le User complet. Le test pin ce comportement actuel. - liste non vide -> reponse paginee (
nextpage= page + 1) ; liste vide ->data: []+ message “No Mission.”
2.8 putInProgress — POST /api/put-inprogress
Section titled “2.8 putInProgress — POST /api/put-inprogress”Auth
- utilisateur non authentifie -> 401
Validation des inputs
mission_idmanquant / non integer / inexistant -> 422mission_codemanquant / non integer -> 422
Gardes metier
- mission non possedee par le user connecte -> 404
Logique metier / effets observables
- delegue a
MissionService::put_inprogress($mission_id, $code) - exception du service -> 500 (“Failed to start mission.”)
- resultat
invalid_code-> 422 (“Invalid mission code.”) - resultat
not_matched-> 422 (“Mission is not in matched state.”) - sinon (mission retournee) -> 200 avec
mission_id
2.9 cancelMissionV2 — POST /api/cancel-missionV2
Section titled “2.9 cancelMissionV2 — POST /api/cancel-missionV2”Auth
- utilisateur non authentifie -> 401
Validation des inputs
mission_idmanquant / non integer / inexistant -> 422reason_idmanquant / non integer / inexistant dansreport_reasons-> 422notenon string -> 422
Gardes metier
- mission dont
owner_id != user-> 403 - mission dont le
statusest hors{open, matched}-> 422 (“Mission cannot be canceled in its current state.”)
Logique metier / effets observables
- delegue a
MissionService::cancelMission($mission, $reason_id, $note); broadcastAdminInstantMissionClosed - exception du service -> 500 (“Failed to cancel mission.”)
- resultat avec
error = mission_already_confirmed-> 422 (“Mission cannot be canceled as it is already confirmed.”) - sinon -> 200
2.10 cancelMissionByWorker — POST /api/cancel-mission-by-worker
Section titled “2.10 cancelMissionByWorker — POST /api/cancel-mission-by-worker”Auth
- utilisateur non authentifie -> 401
Validation des inputs
mission_idmanquant / non integer / inexistant -> 422
Gardes metier
- pas de
MissionAssignmentpendingpour (cette mission, le worker connecte) -> 422 (“You Could not cancel mission .”) - mission
status = cancel-> 422 (“Mission is already canceled.”) - mission
status = in_progress-> 422 (“Mission is in progress and cannot be canceled.”)
Logique metier / effets observables
- delegue a
MissionService::releaseAssignment($mission, $worker_id, initiatedByWorker: true) - exception du service -> 500 (“Failed to cancel assignment.”)
- succes -> 200 (“Mission reopened successfully after worker cancellation.”)
2.11 cancelMissionByOwner — POST /api/cancel-mission-by-owner
Section titled “2.11 cancelMissionByOwner — POST /api/cancel-mission-by-owner”Auth
- utilisateur non authentifie -> 401
Validation des inputs
mission_idmanquant / non integer / inexistant -> 422
Gardes metier
- pas de
MissionAssignmentpendingsur la mission (peu importe le worker) -> 422 (“You Could not cancel mission .”) - mission
status = cancel-> 422 (“Mission is already canceled.”) - mission
status = in_progress-> 422 (“Mission is in progress and cannot be canceled.”) - manquant : aucun controle que le user connecte est l’owner de la mission (cf section 1) — comportement actuel a pinner, fix d’autorisation a prevoir
Logique metier / effets observables
- delegue a
MissionService::releaseAssignment($mission, $worker_id, initiatedByWorker: false);worker_id= celui de l’assignment pending - exception du service -> 500 (“Failed to cancel assignment.”)
- succes -> 200 (“Mission reopened successfully after owner cancellation.”)
2.12 updateMyMission — POST /api/update-my-mission
Section titled “2.12 updateMyMission — POST /api/update-my-mission”Auth
- utilisateur non authentifie -> 401
Validation des inputs
mission_idmanquant / non integer / inexistant -> 422dateformat invalide -> 422start_time/end_timepas au formatH:i:s-> 422duration/rate_per_hournon numeric -> 422descriptionnon string -> 422
Gardes metier
- mission dont
owner_id !== user-> 403 - mission dont le
statusest hors{open, pending}-> 422 (mission non editable)
Logique metier / effets observables
- update partiel : seuls
date/start_time/end_time/rate_per_hour/descriptionpresents dans le payload sont modifies ; un champ absent garde sa valeur - recalcul
durationviaMissionService::computeDurationsistart_timeouend_timeest fourni (valeurs fusionnees avec l’existant) - si la mission est
openapres save ->SyncMissionMatching::dispatch($mission->id, contentChanged: true) - reponse : 200 avec la mission rechargee (relations
looking,owner,feedbacks)