diff --git a/src/appengine/handlers/fuzzers.py b/src/appengine/handlers/fuzzers.py index c5d557aebab..33d93e73f16 100644 --- a/src/appengine/handlers/fuzzers.py +++ b/src/appengine/handlers/fuzzers.py @@ -195,7 +195,8 @@ def apply_fuzzer_changes(self, fuzzer, upload_info): fuzzer.external_contribution = bool(external_contribution) fuzzer.differential = bool(differential) fuzzer.additional_environment_string = environment_string - fuzzer.timestamp = datetime.datetime.utcnow() + fuzzer.timestamp = datetime.datetime.now(tz=datetime.timezone.utc).replace( + tzinfo=None) fuzzer.data_bundle_name = data_bundle_name # Update only if a new archive is provided. @@ -251,6 +252,8 @@ def post(self): fuzzer = data_types.Fuzzer() fuzzer.name = name fuzzer.revision = 0 + fuzzer.created_at = datetime.datetime.now(tz=datetime.timezone.utc).replace( + tzinfo=None) return self.apply_fuzzer_changes(fuzzer, upload_info) diff --git a/src/clusterfuzz/_internal/datastore/data_types.py b/src/clusterfuzz/_internal/datastore/data_types.py index e15966b61bf..ab0eb20408b 100644 --- a/src/clusterfuzz/_internal/datastore/data_types.py +++ b/src/clusterfuzz/_internal/datastore/data_types.py @@ -261,6 +261,10 @@ class Blacklist(Model): class Fuzzer(Model): """Represents a fuzzer.""" + # Created at timestamp. Not set for fuzzers created before this field was + # added. + created_at = ndb.DateTimeProperty() + # Additionally allows '.' and '@' over NAME_CHECK_REGEX. VALID_NAME_REGEX = re.compile(r'^[a-zA-Z0-9_@.-]+$') diff --git a/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py b/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py index 997bf7d2f52..6ddf0cc13a0 100644 --- a/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py +++ b/src/clusterfuzz/_internal/tests/appengine/handlers/fuzzers_test.py @@ -17,8 +17,14 @@ import datetime import unittest +import flask +import webtest + from clusterfuzz._internal.datastore import data_types +from clusterfuzz._internal.tests.test_libs import helpers as test_helpers +from clusterfuzz._internal.tests.test_libs import test_utils from handlers import fuzzers +from libs import form class BaseEditHandlerTest(unittest.TestCase): @@ -56,3 +62,48 @@ def test_get_fuzzer_state_str(self): self.assertNotIn('sample_testcase:', state_str) self.assertNotIn('stats_columns:', state_str) self.assertNotIn('stats_column_descriptions:', state_str) + + +@test_utils.with_cloud_emulators('datastore') +class CreateHandlerTest(unittest.TestCase): + """Tests CreateHandler creates a new Fuzzer""" + + def setUp(self): + test_helpers.patch(self, [ + 'libs.access.has_access', + 'libs.auth.get_current_user', + 'libs.helpers.get_user_email', + 'handlers.fuzzers.datetime', + 'handlers.fuzzers.CreateHandler.get_upload', + 'handlers.fuzzers.CreateHandler.apply_fuzzer_changes', + ]) + self.mock.has_access.return_value = True + self.mock.get_current_user().email = 'test@user.com' + self.mock.get_user_email.return_value = 'test@user.com' + + self.mock_time = datetime.datetime(2026, 1, 1, tzinfo=None) + self.mock.datetime.datetime.now.return_value = self.mock_time + self.mock.datetime.timezone = datetime.timezone + + flaskapp = flask.Flask('testflask') + flaskapp.add_url_rule('/', view_func=fuzzers.CreateHandler.as_view('/')) + self.app = webtest.TestApp(flaskapp) + + def test_create_fuzzer(self): + """Test create fuzzer with basic properties.""" + fuzzer_name = 'test_fuzzer' + + resp = self.app.post_json('/', { + 'csrf_token': form.generate_csrf_token(), + 'name': fuzzer_name, + }) + + self.assertEqual(200, resp.status_int) + + self.mock.apply_fuzzer_changes.assert_called_once() + + fuzzer = self.mock.apply_fuzzer_changes.call_args[0][1] + + self.assertEqual(fuzzer.name, fuzzer_name) + self.assertEqual(fuzzer.revision, 0) + self.assertEqual(fuzzer.created_at, self.mock_time)