1+ /**
2+ * userdata.test.js — Pruebas de integración de /api/user/data
3+ * (createUserDataRouter) con un Supabase fake en memoria que simula
4+ * la tabla user_data (favorites/settings/history como JSON strings).
5+ *
6+ * Cubre: PATCH de favoritos (true agrega / false elimina), historial con
7+ * cap y validación, settings con región "loc" y sanitización.
8+ */
9+ 'use strict' ;
10+
11+ const { test } = require ( 'node:test' ) ;
12+ const assert = require ( 'node:assert' ) ;
13+
14+ function makeApp ( ) {
15+ const express = require ( 'express' ) ;
16+ const { createUserDataRouter } = require ( '../modules/userdata' ) ;
17+ let row = null ;
18+ const supabase = {
19+ from ( ) {
20+ return {
21+ select ( ) { return sel ; } ,
22+ upsert ( obj ) { row = obj ; return { error : null } ; } ,
23+ } ;
24+ } ,
25+ } ;
26+ const sel = {
27+ eq ( ) { return this ; } ,
28+ limit ( ) { return this ; } ,
29+ async maybeSingle ( ) { return { data : row , error : null } ; } ,
30+ } ;
31+ const app = express ( ) ;
32+ app . use ( express . json ( ) ) ;
33+ app . use ( ( req , res , next ) => { req . authUser = { id : 'u-test' , email : 't@x.dev' } ; next ( ) ; } ) ;
34+ app . use ( '/api/user' , createUserDataRouter ( supabase ) ) ;
35+ const state = { getRow : ( ) => row } ;
36+ return { app, state } ;
37+ }
38+
39+ function request ( app , method , path , body , token ) {
40+ const http = require ( 'http' ) ;
41+ const server = app . listen ( 0 ) ;
42+ const port = server . address ( ) . port ;
43+ return new Promise ( ( resolve , reject ) => {
44+ const payload = body ? JSON . stringify ( body ) : null ;
45+ const req = http . request ( { host : '127.0.0.1' , port, path, method, headers : {
46+ 'Content-Type' : 'application/json' ,
47+ ...( token ? { Authorization : 'Bearer ' + token } : { } ) ,
48+ } } , ( res ) => {
49+ let data = '' ;
50+ res . on ( 'data' , ( d ) => { data += d ; } ) ;
51+ res . on ( 'end' , ( ) => { server . close ( ) ; resolve ( { status : res . statusCode , body : JSON . parse ( data ) } ) ; } ) ;
52+ } ) ;
53+ req . on ( 'error' , reject ) ;
54+ if ( payload ) req . write ( payload ) ;
55+ req . end ( ) ;
56+ } ) ;
57+ }
58+
59+ test ( 'PATCH de favoritos: true agrega, false elimina, ausentes no cambian' , async ( ) => {
60+ const { app, state } = makeApp ( ) ;
61+ let r = await request ( app , 'PUT' , '/api/user/data' , { favorites : { pixa : true , pixb : true } } ) ;
62+ assert . equal ( r . status , 200 ) ;
63+ assert . deepEqual ( r . body . data . favorites , { pixa : true , pixb : true } ) ;
64+ assert . deepEqual ( JSON . parse ( state . getRow ( ) . favorites ) , { pixa : true , pixb : true } ) ;
65+
66+ r = await request ( app , 'PUT' , '/api/user/data' , { favorites : { pixa : false } } ) ;
67+ assert . equal ( r . status , 200 ) ;
68+ // pixa se elimina; pixb se preserva (no fue mencionado)
69+ assert . deepEqual ( JSON . parse ( state . getRow ( ) . favorites ) , { pixb : true } ) ;
70+
71+ r = await request ( app , 'GET' , '/api/user/data' , null ) ;
72+ assert . equal ( r . status , 200 ) ;
73+ assert . deepEqual ( r . body . data . favorites , { pixb : true } ) ;
74+ } ) ;
75+
76+ test ( 'historial: reemplaza campo, valida shape y cap de 40' , async ( ) => {
77+ const { app } = makeApp ( ) ;
78+ // input válido con 45 ítems (40 válidos únicos + duplicados fuera de cap)
79+ const h = [ ] ;
80+ for ( let i = 0 ; i < 46 ; i ++ ) h . push ( { id : 'tool' + ( i % 45 ) , ts : i } ) ;
81+ let r = await request ( app , 'PUT' , '/api/user/data' , { history : h } ) ;
82+ assert . equal ( r . status , 200 ) ;
83+ assert . equal ( r . body . data . history . length , 40 ) ;
84+
85+ r = await request ( app , 'PUT' , '/api/user/data' , { history : 'x' } ) ;
86+ assert . equal ( r . status , 400 ) ;
87+
88+ r = await request ( app , 'PUT' , '/api/user/data' , { history : [ { id : 'a/b' , ts : 1 } ] } ) ;
89+ assert . equal ( r . status , 200 ) ;
90+ assert . equal ( r . body . data . history . length , 0 ) ;
91+ } ) ;
92+
93+ test ( 'settings: primitivos + region loc; objetos anidados se descartan' , async ( ) => {
94+ const { app, state } = makeApp ( ) ;
95+ const r = await request ( app , 'PUT' , '/api/user/data' , {
96+ settings : {
97+ theme : 'dark' ,
98+ font : 'md' ,
99+ lang : 'es' ,
100+ loc : { lat : 14.6 , lon : - 90.5 , city : 'Ciudad de Guatemala' , country : 'Guatemala' , src : 'search' } ,
101+ nested : { x : 1 } ,
102+ arr : [ 1 , 2 ] ,
103+ } ,
104+ } ) ;
105+ assert . equal ( r . status , 200 ) ;
106+ const saved = JSON . parse ( state . getRow ( ) . settings ) ;
107+ assert . equal ( saved . theme , 'dark' ) ;
108+ assert . deepEqual ( saved . loc , { lat : 14.6 , lon : - 90.5 , city : 'Ciudad de Guatemala' , country : 'Guatemala' , src : 'search' } ) ;
109+ assert . equal ( saved . nested , undefined ) ;
110+ } ) ;
111+
112+ test ( 'sin payload → 400; tipos inválidos → 400' , async ( ) => {
113+ const { app } = makeApp ( ) ;
114+ let r = await request ( app , 'PUT' , '/api/user/data' , { } ) ;
115+ assert . equal ( r . status , 400 ) ;
116+ r = await request ( app , 'PUT' , '/api/user/data' , { favorites : [ 1 , 2 ] } ) ;
117+ assert . equal ( r . status , 400 ) ;
118+ r = await request ( app , 'PUT' , '/api/user/data' , { settings : 'nope' } ) ;
119+ assert . equal ( r . status , 400 ) ;
120+ } ) ;
0 commit comments