Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions packages/dart/lib/src/objects/parse_installation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,37 @@ class ParseInstallation extends ParseObject {

String _getNameLocalTimeZone() {
tz.initializeTimeZones();
var locations = tz.timeZoneDatabase.locations;

Duration offset = DateTime.now().timeZoneOffset;
String name = "";
// Capture once to avoid a DST-transition race between the two reads.
final DateTime now = DateTime.now();
Comment thread
AdrianCurtin marked this conversation as resolved.
Outdated

locations.forEach((key, value) {
for (var element in value.zones) {
if (element.offset == offset) {
name = value.name;
break;
}
// Prefer the OS-reported zone name when it's a valid IANA location
// (e.g. "America/New_York" on macOS/Linux/iOS/Android). Avoids the
// ambiguity of matching by offset, where many zones share an offset.
final String systemName = now.timeZoneName;
if (tz.timeZoneDatabase.locations.containsKey(systemName)) {
return systemName;
}

// Fall back to a location whose *current* zone matches the local
// offset. The previous implementation scanned every historical zone
// (LMT, pre-DST, etc.) and compared a Duration against an int offset,
// which on timezone <0.11.0 is always false and produced "".
final int localOffsetMs = now.timeZoneOffset.inMilliseconds;
for (final location in tz.timeZoneDatabase.locations.values) {
final dynamic zoneOffset = location.currentTimeZone.offset;
final int zoneOffsetMs = zoneOffset is Duration
? zoneOffset.inMilliseconds
: zoneOffset as int;
Comment thread
AdrianCurtin marked this conversation as resolved.
Outdated
if (zoneOffsetMs == localOffsetMs) {
return location.name;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
return name;
}

// Last resort: return whatever the OS gave us rather than "".
// Note: on Windows/Web this may be a non-IANA name (e.g.
// "Pacific Standard Time" or "EDT"), but it's still better than "".
return systemName;
}

@override
Expand Down
88 changes: 68 additions & 20 deletions packages/dart/test/parse_installation_test.dart
Original file line number Diff line number Diff line change
@@ -1,30 +1,78 @@
import 'package:parse_server_sdk/parse_server_sdk.dart';
import 'package:test/test.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:timezone/timezone.dart' as tz;

Future<void> _initParse() => Parse().initialize(
'appId',
'https://example.com',
debug: true,
fileDirectory: 'someDirectory',
appName: 'appName',
appPackageName: 'somePackageName',
appVersion: 'someAppVersion',
);

void main() {
test('should return true for exist TimeZone.', () async {
// arrange
await Parse().initialize(
'appId',
'https://example.com',
debug: true,
// to prevent automatic detection
fileDirectory: 'someDirectory',
// to prevent automatic detection
appName: 'appName',
// to prevent automatic detection
appPackageName: 'somePackageName',
// to prevent automatic detection
appVersion: 'someAppVersion',
setUpAll(() async {
await _initParse();
});

test('installation has a timeZone field', () async {
final installation = await ParseInstallation.currentInstallation();
expect(installation.containsKey(keyTimeZone), isTrue);
});

// Regression: the SDK previously compared `int == Duration` when matching
// offsets against the timezone database. On timezone <0.11.0 that's always
// false, so the timeZone field was persisted as "". See
// _getNameLocalTimeZone() in parse_installation.dart.
test('installation timeZone is not empty', () async {
final installation = await ParseInstallation.currentInstallation();
final tzValue = installation.get<String>(keyTimeZone);
expect(tzValue, isNotNull);
expect(
tzValue,
isNotEmpty,
reason: 'Regression: timeZone was being stored as "".',
);
});

test('installation timeZone is an IANA name or the OS-reported name',
() async {
tz.initializeTimeZones();
Comment thread
AdrianCurtin marked this conversation as resolved.
Outdated
final installation = await ParseInstallation.currentInstallation();
final tzValue = installation.get<String>(keyTimeZone)!;

final bool isIana = tz.timeZoneDatabase.locations.containsKey(tzValue);
final bool matchesSystem = tzValue == DateTime.now().timeZoneName;

expect(
isIana || matchesSystem,
isTrue,
reason:
'timeZone "$tzValue" should be an IANA zone or the OS-reported '
'name (fallback for Windows/Web).',
);
});

test('when timeZone is matched via offset, its offset equals the local offset',
() async {
tz.initializeTimeZones();
Comment thread
AdrianCurtin marked this conversation as resolved.
Outdated
final installation = await ParseInstallation.currentInstallation();
final tzValue = installation.get<String>(keyTimeZone)!;

// act
final ParseInstallation installation =
await ParseInstallation.currentInstallation();
final location = tz.timeZoneDatabase.locations[tzValue];
if (location == null) {
// OS-reported, non-IANA fallback (Windows/Web). Nothing to verify.
return;
}

dynamic actualHasTimeZoneResult = installation.containsKey(keyTimeZone);
final dynamic zoneOffset = location.currentTimeZone.offset;
final int zoneOffsetMs = zoneOffset is Duration
? zoneOffset.inMilliseconds
: zoneOffset as int;

// assert
expect(actualHasTimeZoneResult, true);
expect(zoneOffsetMs, equals(DateTime.now().timeZoneOffset.inMilliseconds));
Comment thread
AdrianCurtin marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});
}