Repository: joke2k/faker

Issue:
Firefox user agents with fixed seed are non-deterministic
* Faker version: tested on 8.14.1
* OS: MacOS

Seeded calls to `user_agent.firefox()` do not produce deterministic results, dependening on *when* the code is executed.

### Steps to reproduce

1. Run
```python
from faker import Faker
faker = Faker()
faker.seed_instance(1)
print(faker.firefox())
# Executing this on Feb 16th 2022 using faker 8.14.1 returned the following:
# 'Mozilla/5.0 (X11; Linux i686; rv:1.9.5.20) Gecko/2012-01-03 17:18:41 Firefox/3.8'
```
2. *Wait a few an unspecified amount of time* (alternatively, set your computer's date to something else far enough from current date).
3. Run step 1 again. The result is different.

### Expected behavior

The output of the code in step 1 should always be the same given the same version of faker and same seed.

### Actual behavior

The output of the code in step 1 changes depending on system time.

### Why this happens

Firefox user agents contain a segment `"Gecko/<timestamp>"`. Faker generates that timestamp by calling `date_time.date_time_between(<hardcoded start time>)`, setting the beginning of the interval but not the end, which defaults to `"now"` (i.e. `datetime.datetime.now()`), changing the interval and *sometimes* the end result .

**Bonus:** The reason you sometimes have to wait for quite long for the value of the seeded call to `firefox()` to change seems to be the underlying call to `randint(a,b)`. When seeded, that function is very insensitive to changes in the right end of the interval. For example, given a fixed seed, `randint(0,100000)` and `randint(0,111111)` return the exact same value. This likely points to the use of moduli in the RNG implementation in that function but that's a different issue and I didn't want to sidetrack myself. That is the reason why the actual bug with user agents is hard to reproduce, it happens because when working with epochs numbers can be quite large.

### Conclusion

I understand why the firefox user agent is generated the way it is, you would not want timestamps in the future or capped to an arbitrary date. But this inevitably breaks any workflow that depends on seeding to generate user agents deterministically (it broke some tests I'm working on). 

For my particular use case I can develop around this (basically by not calling `user_agent()` ever) but I can't come up with a simple solution that doesn't simply cap the timestamps of the `"Gecko"` strings. What do repo maintainers/users think?


Discussion:
jhbuhrman: Fixed issue from `make lint`.

Patch:
diff --git a/faker/providers/user_agent/__init__.py b/faker/providers/user_agent/__init__.py
index e49a113c..02bfd0ff 100644
--- a/faker/providers/user_agent/__init__.py
+++ b/faker/providers/user_agent/__init__.py
@@ -1,9 +1,11 @@
 import string
 
-from datetime import datetime
+from datetime import datetime, timedelta
 
 from .. import BaseProvider, ElementsType
 
+_DT_ALMOST_MAX = datetime.max - timedelta(1.0)
+
 
 class Provider(BaseProvider):
     """Implement default user agent provider for Faker."""
@@ -187,14 +189,14 @@ class Provider(BaseProvider):
         """Generate a Mozilla Firefox web browser user agent string."""
         ver: ElementsType = (
             (
-                f"Gecko/{self.generator.date_time_between(datetime(2011, 1, 1))} "
+                f"Gecko/{self.generator.date_time_between(datetime(2011, 1, 1), _DT_ALMOST_MAX)} "
                 f"Firefox/{self.generator.random.randint(4, 15)}.0"
             ),
             (
-                f"Gecko/{self.generator.date_time_between(datetime(2010, 1, 1))} "
+                f"Gecko/{self.generator.date_time_between(datetime(2010, 1, 1), _DT_ALMOST_MAX)} "
                 f"Firefox/3.6.{self.generator.random.randint(1, 20)}"
             ),
-            f"Gecko/{self.generator.date_time_between(datetime(2010, 1, 1))} Firefox/3.8",
+            f"Gecko/{self.generator.date_time_between(datetime(2010, 1, 1), _DT_ALMOST_MAX)} Firefox/3.8",
         )
         tmplt_win: str = "({0}; {1}; rv:1.9.{2}.20) {3}"
         tmplt_lin: str = "({0}; rv:1.9.{1}.20) {2}"


Test patch:
diff --git a/tests/providers/test_user_agent.py b/tests/providers/test_user_agent.py
index 5a484232..05809687 100644
--- a/tests/providers/test_user_agent.py
+++ b/tests/providers/test_user_agent.py
@@ -1,7 +1,11 @@
+import datetime as dt
 import re
 
 from typing import Pattern
 
+from freezegun import freeze_time
+
+from faker import Faker
 from faker.providers.user_agent import Provider as UaProvider
 
 
@@ -14,6 +18,7 @@ class TestUserAgentProvider:
         r"^(?P<apple_device>.*?); CPU \1 OS " + r"(?P<ios_version>\d+(?:_\d){0,2}) like Mac OS X"
     )
     mac_token_pattern: Pattern = re.compile(r"Macintosh; (?P<mac_processor>.*?) Mac OS X 10_([5-9]|1[0-2])_(\d)")
+    one_day = dt.timedelta(1.0)
 
     def test_android_platform_token(self, faker, num_samples):
         for _ in range(num_samples):
@@ -30,3 +35,35 @@ class TestUserAgentProvider:
         for _ in range(num_samples):
             match = self.mac_token_pattern.fullmatch(faker.mac_platform_token())
             assert match.group("mac_processor") in UaProvider.mac_processors
+
+    def test_firefox_deterministic_output(self, faker: Faker, num_samples: int) -> None:
+        """Check whether ``faker.firefox()`` is deterministic, given the same seed."""
+
+        for _ in range(num_samples):
+
+            # GIVEN a (new) random seed
+            seed = faker.random.random()
+
+            # AND a DevOpsTester using a Faker instance seeded with this seed
+
+            # It is a bit tricky to feed the faker with its own random
+            # value, but it is sufficient for this particular test
+            faker.seed_instance(seed)
+
+            # AND the DevOpsTester using the fake library tomorrow
+            with freeze_time(dt.datetime.now() + self.one_day):
+
+                # AND the DevOpsTester requests a faked Mozilla Firefox web browser user agent (str)
+                fake_firefox_ua_output_tomorrow = faker.firefox()
+
+            # WHEN the DevOpsTester would use the fake library with the same seed
+            faker.seed_instance(seed)
+
+            # AND the DevOpsTester would use the fake library some time later
+            with freeze_time(dt.datetime.max - self.one_day):
+
+                # AND the DevOpsTester requests again faked Mozilla Firefox web browser user agent
+                fake_firefox_ua_output_much_later = faker.firefox()
+
+            # THEN the later Firefox U/A output should (always) be equal to the previous one
+            assert fake_firefox_ua_output_much_later == fake_firefox_ua_output_tomorrow