@@ -162,5 +162,222 @@ void main() {
162162 });
163163 });
164164 });
165+
166+ test ("Should keep subscription info when total and expire are omitted" , () {
167+ // Panels routinely omit total (unlimited plans) and expire (no-expiry plans). The header must
168+ // still produce a SubscriptionInfo, and the support-url / profile-web-page-url tiles parsed
169+ // alongside it must still be applied. Regression test for #2242.
170+ final headers = < String , List <String >> {
171+ "profile-title" : ["title" ],
172+ "subscription-userinfo" : ["upload=100;download=200" ],
173+ "profile-web-page-url" : [validBaseUrl],
174+ "support-url" : [validSupportUrl],
175+ };
176+ final fixedHeaders = headers.map ((key, value) {
177+ if (value.length == 1 ) return MapEntry (key, value.first);
178+ return MapEntry (key, value);
179+ });
180+ final allHeaders = ProfileParser .populateHeaders (content: '' , remoteHeaders: fixedHeaders);
181+ expect (allHeaders.isRight (), true );
182+ allHeaders.match ((l) {}, (r) {
183+ final profile = ProfileParser .parse (
184+ tempFilePath: '' ,
185+ profile: RemoteProfileEntity (
186+ id: const Uuid ().v4 (),
187+ active: true ,
188+ name: '' ,
189+ url: validBaseUrl,
190+ lastUpdate: DateTime .now (),
191+ populatedHeaders: r,
192+ ),
193+ );
194+ expect (profile.isRight (), true );
195+ profile.match ((l) {}, (r) {
196+ expect (r is RemoteProfileEntity , true );
197+ r.map (
198+ remote: (rp) {
199+ expect (rp.subInfo, isNotNull);
200+ expect (rp.subInfo! .upload, equals (100 ));
201+ expect (rp.subInfo! .download, equals (200 ));
202+ expect (rp.subInfo! .total, equals (ProfileParser .infiniteTrafficThreshold + 1 ));
203+ expect (
204+ rp.subInfo! .expire,
205+ equals (DateTime .fromMillisecondsSinceEpoch (ProfileParser .infiniteTimeThreshold * 1000 )),
206+ );
207+ expect (rp.subInfo! .webPageUrl, equals (validBaseUrl));
208+ expect (rp.subInfo! .supportUrl, equals (validSupportUrl));
209+ },
210+ local: (lp) {},
211+ );
212+ });
213+ });
214+ });
215+
216+ test ("Should not drop the profile when a subscription-userinfo segment is malformed" , () {
217+ // A segment without '=' (here "garbage") previously threw and failed the entire profile parse.
218+ // It must now be skipped while the valid fields are still parsed.
219+ final headers = < String , List <String >> {
220+ "profile-title" : ["title" ],
221+ "subscription-userinfo" : ["upload=100;download=200;garbage;total=5000;expire=1704054600" ],
222+ };
223+ final fixedHeaders = headers.map ((key, value) {
224+ if (value.length == 1 ) return MapEntry (key, value.first);
225+ return MapEntry (key, value);
226+ });
227+ final allHeaders = ProfileParser .populateHeaders (content: '' , remoteHeaders: fixedHeaders);
228+ expect (allHeaders.isRight (), true );
229+ allHeaders.match ((l) {}, (r) {
230+ final profile = ProfileParser .parse (
231+ tempFilePath: '' ,
232+ profile: RemoteProfileEntity (
233+ id: const Uuid ().v4 (),
234+ active: true ,
235+ name: '' ,
236+ url: validBaseUrl,
237+ lastUpdate: DateTime .now (),
238+ populatedHeaders: r,
239+ ),
240+ );
241+ expect (profile.isRight (), true );
242+ profile.match ((l) {}, (r) {
243+ expect (r is RemoteProfileEntity , true );
244+ r.map (
245+ remote: (rp) {
246+ expect (rp.subInfo, isNotNull);
247+ expect (rp.subInfo! .upload, equals (100 ));
248+ expect (rp.subInfo! .download, equals (200 ));
249+ expect (rp.subInfo! .total, equals (5000 ));
250+ expect (
251+ rp.subInfo! .expire,
252+ equals (DateTime .fromMillisecondsSinceEpoch (1704054600 * 1000 )),
253+ );
254+ },
255+ local: (lp) {},
256+ );
257+ });
258+ });
259+ });
260+
261+ test ("Should fall back to unlimited traffic when only total is omitted" , () {
262+ final headers = < String , List <String >> {
263+ "profile-title" : ["title" ],
264+ "subscription-userinfo" : ["upload=100;download=200;expire=1704054600" ],
265+ };
266+ final fixedHeaders = headers.map ((key, value) {
267+ if (value.length == 1 ) return MapEntry (key, value.first);
268+ return MapEntry (key, value);
269+ });
270+ final allHeaders = ProfileParser .populateHeaders (content: '' , remoteHeaders: fixedHeaders);
271+ expect (allHeaders.isRight (), true );
272+ allHeaders.match ((l) {}, (r) {
273+ final profile = ProfileParser .parse (
274+ tempFilePath: '' ,
275+ profile: RemoteProfileEntity (
276+ id: const Uuid ().v4 (),
277+ active: true ,
278+ name: '' ,
279+ url: validBaseUrl,
280+ lastUpdate: DateTime .now (),
281+ populatedHeaders: r,
282+ ),
283+ );
284+ expect (profile.isRight (), true );
285+ profile.match ((l) {}, (r) {
286+ expect (r is RemoteProfileEntity , true );
287+ r.map (
288+ remote: (rp) {
289+ expect (rp.subInfo, isNotNull);
290+ expect (rp.subInfo! .total, equals (ProfileParser .infiniteTrafficThreshold + 1 ));
291+ expect (rp.subInfo! .expire, equals (DateTime .fromMillisecondsSinceEpoch (1704054600 * 1000 )));
292+ },
293+ local: (lp) {},
294+ );
295+ });
296+ });
297+ });
298+
299+ test ("Should fall back to no-expiry when only expire is omitted" , () {
300+ final headers = < String , List <String >> {
301+ "profile-title" : ["title" ],
302+ "subscription-userinfo" : ["upload=100;download=200;total=5000" ],
303+ };
304+ final fixedHeaders = headers.map ((key, value) {
305+ if (value.length == 1 ) return MapEntry (key, value.first);
306+ return MapEntry (key, value);
307+ });
308+ final allHeaders = ProfileParser .populateHeaders (content: '' , remoteHeaders: fixedHeaders);
309+ expect (allHeaders.isRight (), true );
310+ allHeaders.match ((l) {}, (r) {
311+ final profile = ProfileParser .parse (
312+ tempFilePath: '' ,
313+ profile: RemoteProfileEntity (
314+ id: const Uuid ().v4 (),
315+ active: true ,
316+ name: '' ,
317+ url: validBaseUrl,
318+ lastUpdate: DateTime .now (),
319+ populatedHeaders: r,
320+ ),
321+ );
322+ expect (profile.isRight (), true );
323+ profile.match ((l) {}, (r) {
324+ expect (r is RemoteProfileEntity , true );
325+ r.map (
326+ remote: (rp) {
327+ expect (rp.subInfo, isNotNull);
328+ expect (rp.subInfo! .total, equals (5000 ));
329+ expect (
330+ rp.subInfo! .expire,
331+ equals (DateTime .fromMillisecondsSinceEpoch (ProfileParser .infiniteTimeThreshold * 1000 )),
332+ );
333+ },
334+ local: (lp) {},
335+ );
336+ });
337+ });
338+ });
339+
340+ test ("Should not throw the whole profile away on non-finite numeric values" , () {
341+ // num.tryParse of "1e999"/"Infinity"/"NaN" yields a non-finite double; double.toInt() throws on
342+ // those. The parser must degrade them gracefully (unlimited sentinels) instead of failing the parse.
343+ final headers = < String , List <String >> {
344+ "profile-title" : ["title" ],
345+ "subscription-userinfo" : ["upload=100;download=200;total=1e999;expire=NaN" ],
346+ };
347+ final fixedHeaders = headers.map ((key, value) {
348+ if (value.length == 1 ) return MapEntry (key, value.first);
349+ return MapEntry (key, value);
350+ });
351+ final allHeaders = ProfileParser .populateHeaders (content: '' , remoteHeaders: fixedHeaders);
352+ expect (allHeaders.isRight (), true );
353+ allHeaders.match ((l) {}, (r) {
354+ final profile = ProfileParser .parse (
355+ tempFilePath: '' ,
356+ profile: RemoteProfileEntity (
357+ id: const Uuid ().v4 (),
358+ active: true ,
359+ name: '' ,
360+ url: validBaseUrl,
361+ lastUpdate: DateTime .now (),
362+ populatedHeaders: r,
363+ ),
364+ );
365+ expect (profile.isRight (), true );
366+ profile.match ((l) {}, (r) {
367+ expect (r is RemoteProfileEntity , true );
368+ r.map (
369+ remote: (rp) {
370+ expect (rp.subInfo, isNotNull);
371+ expect (rp.subInfo! .total, equals (ProfileParser .infiniteTrafficThreshold + 1 ));
372+ expect (
373+ rp.subInfo! .expire,
374+ equals (DateTime .fromMillisecondsSinceEpoch (ProfileParser .infiniteTimeThreshold * 1000 )),
375+ );
376+ },
377+ local: (lp) {},
378+ );
379+ });
380+ });
381+ });
165382 });
166383}
0 commit comments