|
| 1 | +const spt = require('supertest'); |
| 2 | +const should = require('should'); |
| 3 | +const testUtils = require('../../../test/testUtils'); |
| 4 | + |
| 5 | +const request = spt(testUtils.url); |
| 6 | +// change these in local testing directly or set env vars (also COUNTLY_CONFIG_HOSTNAME should be set with port) |
| 7 | +let API_KEY_ADMIN = testUtils.get("API_KEY_ADMIN"); |
| 8 | +let APP_KEY = testUtils.get('APP_KEY'); |
| 9 | +let APP_ID = testUtils.get("APP_ID"); |
| 10 | + |
| 11 | +describe('CSV/Array and JSON validation', function() { |
| 12 | + function unescapeHtml(str) { |
| 13 | + if (typeof str !== 'string') { |
| 14 | + return str; |
| 15 | + } |
| 16 | + return str.replace(/"/g, '"') |
| 17 | + .replace(/&/g, '&') |
| 18 | + .replace(/</g, '<') |
| 19 | + .replace(/>/g, '>') |
| 20 | + .replace(/'/g, "'"); |
| 21 | + } |
| 22 | + before(function(done) { |
| 23 | + const enforcement = { |
| 24 | + eb: true, |
| 25 | + upb: true, |
| 26 | + sb: true, |
| 27 | + esb: true |
| 28 | + }; |
| 29 | + |
| 30 | + request |
| 31 | + .post('/i/sdk-config/update-enforcement') |
| 32 | + .query({ api_key: API_KEY_ADMIN, app_id: APP_ID, enforcement: JSON.stringify(enforcement) }) |
| 33 | + .expect(200) |
| 34 | + .end(function(err, res) { |
| 35 | + should.not.exist(err); |
| 36 | + res.body.should.have.property('result', 'Success'); |
| 37 | + done(); |
| 38 | + }); |
| 39 | + }); |
| 40 | + |
| 41 | + it('1. should save arrays for eb/upb/sb and objects for esb when provided as proper types', function(done) { |
| 42 | + const parameter = { |
| 43 | + eb: ['a', 'b, c', ' d '], |
| 44 | + upb: ['user_prop_1', 'user,prop,2'], |
| 45 | + sb: ['seg1', 'seg2'], |
| 46 | + esb: { 'event1': ['a', 'b'] } |
| 47 | + }; |
| 48 | + |
| 49 | + request |
| 50 | + .post('/i/sdk-config/update-parameter') |
| 51 | + .send({ api_key: API_KEY_ADMIN, app_id: APP_ID, parameter: JSON.stringify(parameter) }) |
| 52 | + .expect(200) |
| 53 | + .end(function(err, res) { |
| 54 | + should.not.exist(err); |
| 55 | + res.body.should.have.property('result', 'Success'); |
| 56 | + |
| 57 | + request |
| 58 | + .get('/o/sdk') |
| 59 | + .query({ method: 'sc', app_key: APP_KEY, device_id: 'test' }) |
| 60 | + .expect(200) |
| 61 | + .end(function(err, res) { |
| 62 | + should.not.exist(err); |
| 63 | + res.body.should.have.property('c'); |
| 64 | + const c = res.body.c; |
| 65 | + c.should.have.property('eb'); |
| 66 | + c.eb.should.be.an.Array(); |
| 67 | + c.eb.should.have.length(3); |
| 68 | + c.eb.should.containEql('b, c'); |
| 69 | + c.eb.should.containEql(' d '); |
| 70 | + c.eb.should.containEql('a'); |
| 71 | + |
| 72 | + c.should.have.property('upb'); |
| 73 | + c.upb.should.be.an.Array(); |
| 74 | + c.upb.should.containEql('user,prop,2'); |
| 75 | + c.upb.should.containEql('user_prop_1'); |
| 76 | + c.upb.should.have.length(2); |
| 77 | + |
| 78 | + c.should.have.property('sb'); |
| 79 | + c.sb.should.be.an.Array(); |
| 80 | + c.sb.should.have.length(2); |
| 81 | + c.sb.should.containEql('seg1'); |
| 82 | + c.sb.should.containEql('seg2'); |
| 83 | + |
| 84 | + c.should.have.property('esb'); |
| 85 | + c.esb.should.be.an.Object(); |
| 86 | + c.esb.should.have.property('event1'); |
| 87 | + c.esb.event1.should.be.an.Array(); |
| 88 | + c.esb.event1.should.have.length(2); |
| 89 | + c.esb.event1.should.containEql('a'); |
| 90 | + c.esb.event1.should.containEql('b'); |
| 91 | + done(); |
| 92 | + }); |
| 93 | + }); |
| 94 | + }); |
| 95 | + |
| 96 | + // TODO: in future we may want to auto-parse CSV strings to arrays when uploaded, but for now front-end does this |
| 97 | + it('2. currently stores CSV strings as strings (server does not auto-parse CSV) and esb string stays string', function(done) { |
| 98 | + const parameter = { |
| 99 | + eb: 'one, "two, too", three', |
| 100 | + esb: 'this is not json' |
| 101 | + }; |
| 102 | + |
| 103 | + request |
| 104 | + .post('/i/sdk-config/update-parameter') |
| 105 | + .send({ api_key: API_KEY_ADMIN, app_id: APP_ID, parameter: JSON.stringify(parameter) }) |
| 106 | + .expect(200) |
| 107 | + .end(function(err, res) { |
| 108 | + should.not.exist(err); |
| 109 | + res.body.should.have.property('result', 'Success'); |
| 110 | + |
| 111 | + request |
| 112 | + .get('/o/sdk') |
| 113 | + .query({ method: 'sc', app_key: APP_KEY, device_id: 'test' }) |
| 114 | + .expect(200) |
| 115 | + .end(function(err, res) { |
| 116 | + should.not.exist(err); |
| 117 | + res.body.should.have.property('c'); |
| 118 | + const c = res.body.c; |
| 119 | + c.should.have.property('eb'); |
| 120 | + c.eb.should.be.a.String(); |
| 121 | + unescapeHtml(c.eb).should.be.exactly('one, "two, too", three'); |
| 122 | + |
| 123 | + c.should.have.property('esb'); |
| 124 | + c.esb.should.be.a.String(); |
| 125 | + c.esb.should.be.exactly('this is not json'); |
| 126 | + done(); |
| 127 | + }); |
| 128 | + }); |
| 129 | + }); |
| 130 | + |
| 131 | + it('3. should reject invalid top-level parameter JSON (string) with 400', function(done) { |
| 132 | + request |
| 133 | + .post('/i/sdk-config/update-parameter') |
| 134 | + .send({ api_key: API_KEY_ADMIN, app_id: APP_ID, parameter: 'invalid json' }) |
| 135 | + .expect(400) |
| 136 | + .end(function(err, res) { |
| 137 | + should.not.exist(err); |
| 138 | + res.body.should.have.property('result', 'Error parsing parameter'); |
| 139 | + done(); |
| 140 | + }); |
| 141 | + }); |
| 142 | +}); |
| 143 | + |
| 144 | +// CSV unit tests |
| 145 | +describe('CSV parse/serialize edge cases', function() { |
| 146 | + // copy pasta methods |
| 147 | + function csvToArray(str) { |
| 148 | + if (typeof str !== 'string') { |
| 149 | + return []; |
| 150 | + } |
| 151 | + return Array.from(str.matchAll(/(?:\s*("(?:[^"]|"")*"|[^,]*?)\s*)(?:,|$)/g)).map(function(m) { |
| 152 | + var val = m[1]; |
| 153 | + if (!val) { |
| 154 | + return null; |
| 155 | + } |
| 156 | + if (val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') { |
| 157 | + val = val.slice(1, -1).replace(/""/g, '"'); |
| 158 | + } |
| 159 | + else { |
| 160 | + val = val.trim(); |
| 161 | + } |
| 162 | + return val.length ? val : null; |
| 163 | + }).filter(function(v) { |
| 164 | + return v !== null; |
| 165 | + }); |
| 166 | + } |
| 167 | + |
| 168 | + function arrayToCsv(arr) { |
| 169 | + if (!Array.isArray(arr)) { |
| 170 | + return ''; |
| 171 | + } |
| 172 | + return arr.filter(function(e) { |
| 173 | + return e != null; |
| 174 | + }).map(function(e) { |
| 175 | + e = String(e); |
| 176 | + // quote if contains comma, quote, newline or carriage return, or starts/ends with whitespace |
| 177 | + if (/[,"\n\r]/.test(e) || /^\s|\s$/.test(e)) { |
| 178 | + return '"' + e.replace(/"/g, '""') + '"'; |
| 179 | + } |
| 180 | + return e; |
| 181 | + }).join(','); |
| 182 | + } |
| 183 | + |
| 184 | + it('handles commas inside quoted fields', function() { |
| 185 | + const s = 'one,"two, too",three'; |
| 186 | + csvToArray(s).should.eql(['one', 'two, too', 'three']); |
| 187 | + arrayToCsv(['one', 'two, too', 'three']).should.eql(s); |
| 188 | + }); |
| 189 | + |
| 190 | + it('handles escaped quotes inside quoted fields', function() { |
| 191 | + const s = 'a,"b""c",d'; |
| 192 | + csvToArray(s).should.eql(['a', 'b"c', 'd']); |
| 193 | + arrayToCsv(['a', 'b"c', 'd']).should.eql(s); |
| 194 | + }); |
| 195 | + |
| 196 | + it('handles newlines inside quoted fields', function() { |
| 197 | + const s = '"line1\nline2",simple'; |
| 198 | + csvToArray(s).should.eql(['line1\nline2', 'simple']); |
| 199 | + arrayToCsv(['line1\nline2', 'simple']).should.eql(s); |
| 200 | + }); |
| 201 | + |
| 202 | + it('drops empty fields', function() { |
| 203 | + const s = 'a,,b,,'; |
| 204 | + csvToArray(s).should.eql(['a', 'b']); |
| 205 | + arrayToCsv(['a', 'b']).should.eql('a,b'); |
| 206 | + }); |
| 207 | + |
| 208 | + it('arrayToCsv quotes fields when necessary and roundtrips correctly', function() { |
| 209 | + const arr = ['simple', 'has,comma', ' hasspace ', 'quotes"inside', 'multi\nline']; |
| 210 | + const csv = arrayToCsv(arr); |
| 211 | + csv.should.be.a.String(); |
| 212 | + csv.should.eql('simple,"has,comma"," hasspace ","quotes""inside","multi\nline"'); |
| 213 | + csvToArray(csv).should.eql(['simple', 'has,comma', ' hasspace ', 'quotes"inside', 'multi\nline']); |
| 214 | + }); |
| 215 | + |
| 216 | + it('arrayToCsv produces empty entries for null/undefined which arrayToCsv will drop', function() { |
| 217 | + const arr = ['a', null, undefined, 'b']; |
| 218 | + const csv = arrayToCsv(arr); |
| 219 | + csv.should.be.a.String(); |
| 220 | + csv.should.eql('a,b'); |
| 221 | + csvToArray(csv).should.eql(['a', 'b']); |
| 222 | + }); |
| 223 | + |
| 224 | + it('handles unicode characters correctly', function() { |
| 225 | + const arr = ['emoji 😊', 'accenté', '中文,文本']; |
| 226 | + const csv = arrayToCsv(arr); |
| 227 | + csv.should.be.a.String(); |
| 228 | + csv.should.eql('emoji 😊,accenté,"中文,文本"'); |
| 229 | + csvToArray(csv).should.eql(['emoji 😊', 'accenté', '中文,文本']); |
| 230 | + }); |
| 231 | + |
| 232 | + it('handles extremely long fields', function() { |
| 233 | + const long = 'x'.repeat(100000); // 100k chars |
| 234 | + const arr = ['start', long, 'end']; |
| 235 | + const csv = arrayToCsv(arr); |
| 236 | + csvToArray(csv).should.eql(['start', long, 'end']); |
| 237 | + }).timeout(5000); |
| 238 | + |
| 239 | + it('handles carriage returns inside quoted fields (CR)', function() { |
| 240 | + const s = '"line1\rline2",after'; |
| 241 | + csvToArray(s).should.eql(['line1\rline2', 'after']); |
| 242 | + arrayToCsv(['line1\rline2', 'after']).should.eql(s); |
| 243 | + }); |
| 244 | + |
| 245 | + it('handles CRLF inside quoted fields (CRLF)', function() { |
| 246 | + const s = '"a\r\nb",c'; |
| 247 | + csvToArray(s).should.eql(['a\r\nb', 'c']); |
| 248 | + arrayToCsv(['a\r\nb', 'c']).should.eql(s); |
| 249 | + }); |
| 250 | +}); |
0 commit comments