예제 #1
0
class TestHelloWorldThings(unittest.TestCase):
    """Basic non-brokenness tests."""
    @freeze_time(datetime.datetime.now())
    def setUp(self):

        # Blank out the test JSON files:
        data_map = {  # todo DRY, this is repeated in every model interface's tests module
            "user_data": "test/testing_mockUserDB.json",
            "datespot_data": "test/testing_mockDatespotDB.json",
            "match_data": "test/testing_mockMatchData.json",
            "review_data": "test/testing_mockReviewData.json",
            "message_data": "test/testing_mockMessageData.json",
            "chat_data": "test/testing_mockChatData.json"
        }
        with open(TEST_JSON_DB_NAME, 'w') as fobj:
            json.dump(data_map, fobj)
            fobj.seek(0)

        # make sure all the test-mock JSONs exist
        for filename in data_map:
            with open(data_map[filename], 'w') as fobj:
                json.dump({}, fobj)
                fobj.seek(0)

        # Instantiate DatabaseAPI object and model interfaces
        self.db = DatabaseAPI(json_map_filename=TEST_JSON_DB_NAME)
        self.user_data = model_interfaces.UserModelInterface(
            json_map_filename=TEST_JSON_DB_NAME
        )  # These are cumbersome, but no implementation code actually needs to ask the DB API for
        self.datespot_data = model_interfaces.DatespotModelInterface(
            json_map_filename=TEST_JSON_DB_NAME
        )  #   ...a model object. DB API having a "get_object()" method would be convenient
        self.match_data = model_interfaces.MatchModelInterface(
            json_map_filename=TEST_JSON_DB_NAME
        )  #  ...for the tests code but have no other use.
        self.review_data = model_interfaces.ReviewModelInterface(
            json_map_filename=TEST_JSON_DB_NAME)
        self.chat_data = model_interfaces.ChatModelInterface(
            json_map_filename=TEST_JSON_DB_NAME)
        self.message_data = model_interfaces.MessageModelInterface(
            json_map_filename=TEST_JSON_DB_NAME)

        # Data for mock users
        self.azura_name = "Azura"
        self.azura_location = (40.73517750328247, -74.00683227856715)
        self.azura_id = "1"
        self.azura_existing_taste_name = "dawn"
        self.azura_existing_taste_strength = 0.9
        self.azura_existing_taste_datapoints = 3
        self.azura_existing_tastes = {  # This is the data in the object's internal format, for testing convenience not for a call to the external create_user()
            self.azura_existing_taste_name: [
                self.azura_existing_taste_strength,
                self.azura_existing_taste_datapoints
            ]
        }

        self.azura_data = {
            "name": self.azura_name,
            "current_location": self.azura_location,
            "force_key": self.azura_id
        }

        self.boethiah_name = "Boethiah"
        self.boethiah_location = (40.76346250260515, -73.98013893542904)
        self.boethiah_id = "2"
        self.boethiah_data = {
            "name": self.boethiah_name,
            "current_location": self.boethiah_location,
            "force_key": self.boethiah_id
        }

        self.hircine_name = "Hircine"
        self.hircine_location = (40.76525023033338, -73.96722141608099)
        self.hircine_id = "3"
        self.hircine_data = {
            "name": self.hircine_name,
            "current_location": self.hircine_location,
            "force_key": self.hircine_id
        }

        # Data for mock Datespot
        self.terrezanos_location = (40.737291166191476, -74.00704685527774)
        self.terrezanos_name = "Terrezano's"
        self.terrezanos_traits = {
            "italian": [1.0, "discrete"],
            "wine": [0.5, 1],
            "pasta": [0.6, 2],
            "NOT FROM PIZZA HUT": [0.01, 2],
            "authentic": [-0.05, 3],
            "warehouse": [1.0, "discrete"]
        }
        self.terrezanos_price_range = 2
        self.terrezanos_hours = [
            [14, 22], [14, 21], [14, 21], [14, 21], [14, 23], [14,
                                                               23], [14, 20]
        ]  # ints in [0..23] representing hours, for now

        self.terrezanos_data = {
            "location": self.terrezanos_location,
            "name": self.terrezanos_name,
            "traits": self.terrezanos_traits,
            "price_range": self.terrezanos_price_range,
            "hours": self.terrezanos_hours,
        }

        self.terrezanos_id = self.db.post_object({
            "object_model_name":
            "datespot",
            "object_data":
            self.terrezanos_data
        })

        # Data for mock Review of Terrezano's

        self.mock_text_positive_relevant = "This was a wonderful place to go on a date. I had the pasta. It was authentic and not from Pizza Hut."
        self.expected_sentiment = 0.1906  # todo hardcoded
        self.expected_relevance = round(1 /
                                        len(self.mock_text_positive_relevant),
                                        4)  # i.e. "date" appears once.
        self.terrezanos_review_data = {
            "datespot_id": self.terrezanos_id,
            "text": self.mock_text_positive_relevant
        }

        # Add three users for use in testing compound objects
        self.db.post_object({
            "object_model_name": "user",
            "object_data": self.azura_data
        })
        self.db.post_object({
            "object_model_name": "user",
            "object_data": self.boethiah_data
        })
        self.db.post_object({
            "object_model_name": "user",
            "object_data": self.hircine_data
        })

        # Data for mock Message and Chat
        self.mock_bilateral_timestamp = time.time()
        self.quick_mock_chat_data = {
            "start_time": time.time(),
            "participant_ids": [self.azura_id, self.boethiah_id]
        }
        self.mock_chat_id_1 = self.db.post_object({
            "object_model_name":
            "chat",
            "object_data":
            self.quick_mock_chat_data
        })  # Need a Chat to create a Message
        self.single_sentence_text = "Worship the Nine, do your duty, and heed the commands of the saints and priests."
        self.expected_sentiment_single_sentence = 0.296  # todo hardcoded

        self.mock_bilateral_message_data = {
            "time_sent": self.mock_bilateral_timestamp,
            "sender_id": self.azura_id,
            "chat_id": self.mock_chat_id_1,
            "text": self.single_sentence_text
        }

        # Add two matches for Azura user

        @freeze_time("2021-05-01 12:00:01")
        def freezetime_match_1(match_data: dict) -> str:
            """Creates the match at a specified timestamp to avoid unittest forcing the timestamps to be identical;
            returns the id string."""
            return self.db.post_object(match_data)

        @freeze_time("2021-05-01 12:00:02")  # One second later
        def freezetime_match_2(match_data: dict) -> str:
            """Creates the second match at a later timestamp."""
            return self.db.post_object(match_data)

        match_data_azura_boethiah = {
            "object_model_name": "match",
            "object_data": {
                "user1_id": self.azura_id,
                "user2_id": self.boethiah_id
            }
        }

        match_data_hircine_azura = {
            "object_model_name": "match",
            "object_data": {
                "user1_id": self.hircine_id,
                "user2_id": self.azura_id
            }
        }

        self.match_id_azura_boethiah = freezetime_match_1(
            match_data_azura_boethiah)
        self.match_id_hircine_azura = freezetime_match_2(
            match_data_hircine_azura)

        self.match_obj_azura_boethiah = self.match_data.lookup_obj(
            self.match_id_azura_boethiah)
        self.match_obj_hircine_azura = self.match_data.lookup_obj(
            self.match_id_hircine_azura)

    def test_init(self):
        """Was an object of the expected type instantiated?"""
        self.assertIsInstance(self.db, DatabaseAPI)

    def test_model_interface_constructor_calls(self):
        """Does the DB interface call the expected model interface for each model name?"""
        expected_interfaces = {
            "user": model_interfaces.UserModelInterface,
            "datespot": model_interfaces.DatespotModelInterface,
            "match": model_interfaces.MatchModelInterface,
            "review": model_interfaces.ReviewModelInterface,
            "message": model_interfaces.MessageModelInterface,
            "chat": model_interfaces.ChatModelInterface
        }
        for model_name in expected_interfaces:
            actual_interface = self.db._model_interface(model_name)
            self.assertIsInstance(actual_interface,
                                  expected_interfaces[model_name])

    def test_validate_model_name(self):
        """Does the validator raise the expected error for a bad model name?"""
        with self.assertRaises(ValueError):
            self.db._validate_model_name("foo")

    ### Tests for post_object() and get_object() ###

    def test_post_obj_user(self):
        talos_name = "Talos"
        talos_location = (40.76346250260515, -73.98013893542904)
        expected_talos_id = "4"
        talos_data = {
            "name": talos_name,
            "current_location": talos_location,
            "force_key": expected_talos_id
        }
        actual_talos_id = self.db.post_object({
            "object_model_name": "user",
            "object_data": talos_data
        })
        self.assertIsInstance(actual_talos_id, str)
        talos_obj = self.user_data.lookup_obj(actual_talos_id)
        self.assertIsInstance(talos_obj, models.User)
        self.assertEqual(expected_talos_id, actual_talos_id)

        with self.assertRaises(
                ValueError
        ):  # Trying with key already in DB should raise error
            talos_data["force_key"] = self.hircine_id  # Used in setUp
            actual_talos_id = self.db.post_object({
                "object_model_name": "user",
                "object_data": talos_data
            })

    def test_post_obj_datespot(self):
        domenicos_location = (40.723889184134926, -73.97613846772394)
        domenicos_name = "Domenico's"
        domenicos_traits = {
            "coffee": [
                1.0, 1
            ],  # todo...So we're imagining this as ~ how good the coffee is, rather than the discrete fact that they do serve coffee?
            "coffee shop": [1.0, "discrete"],
            "gourmet": [0.25, 1],
            "americano": [0.15, 1],
            "knows coffee": [0.3, 1],
            "bricks": [0.6, 1],
            "burger juice": [0.9, 1]
        }
        domenicos_price_range = 1
        domenicos_hours = [[8, 19], [8, 19], [8, 19], [8, 19], [8, 19],
                           [8, 19], [10, 17]]

        domenicos_data = {
            "location": domenicos_location,
            "name": domenicos_name,
            "traits": domenicos_traits,
            "price_range": domenicos_price_range,
            "hours": domenicos_hours
        }

        domenicos_id = self.db.post_object({
            "object_model_name": "datespot",
            "object_data": domenicos_data
        })
        domenicos_obj = self.datespot_data.lookup_obj(domenicos_id)

        self.assertIsInstance(domenicos_obj, models.Datespot)

    def test_post_obj_match(self):
        match_data = {"user1_id": self.azura_id, "user2_id": self.boethiah_id}
        match_id = self.db.post_object({
            "object_model_name": "match",
            "object_data": match_data
        })
        match_obj = self.match_data.lookup_obj(match_id)
        self.assertIsInstance(match_obj, models.Match)

    def test_post_obj_review(self):
        review_id = self.db.post_object({
            "object_model_name":
            "review",
            "object_data":
            self.terrezanos_review_data
        })
        review_obj = self.review_data.lookup_obj(review_id)
        self.assertIsInstance(review_obj, models.Review)

    def test_post_obj_message(self):
        message_id = self.db.post_object({
            "object_model_name":
            "message",
            "object_data":
            self.mock_bilateral_message_data
        })
        message_obj = self.message_data.lookup_obj(message_id)
        self.assertIsInstance(message_obj, models.Message)

    def test_post_obj_chat(self):
        chat_id = self.db.post_object({
            "object_model_name": "chat",
            "object_data": self.quick_mock_chat_data
        })
        chat_obj = self.chat_data.lookup_obj(chat_id)
        self.assertIsInstance(chat_obj, models.Chat)

    ### Tests for put_json() ###

    def test_put_json_update_user(self):

        # Test updating a User's location:
        new_data = {
            "current_location": (40.737291166191476, -74.00704685527774),
        }
        args_data = {
            "object_model_name": "user",
            "object_id": self.azura_id,
            "update_data": new_data
        }
        self.db.put_data(args_data)
        azura_obj = self.user_data.lookup_obj(self.azura_id)
        self.assertAlmostEqual(new_data["current_location"],
                               azura_obj.current_location)

    def test_updating_unsupported_model_raises_error(self):
        """Does attempting to update a model for which updates aren't supported raise the 
        expected error?"""
        unsupported_models = ["review", "message"]
        arbitrary_object_id = "a"
        arbitrary_data = {"foo": "bar"}
        for model in unsupported_models:
            with self.assertRaises(ValueError):
                self.db.put_data({
                    "object_model_name": model,
                    "object_id": arbitrary_object_id,
                    "update_data": arbitrary_data
                })

    # TODO complete for other models and their main anticipated update cases

    ### Tests for post_decision() ###
    def test_post_yes_decision_no_match(self):
        """Does posting a "yes" decision that doesn't create a match return the expected JSON?"""
        decision_yes_data = {
            "user_id": self.azura_id,
            "candidate_id": self.boethiah_id,
            "outcome": True
        }
        expected_response_data = {"match_created": False}
        actual_response_data = self.db.post_decision(decision_yes_data)
        self.assertEqual(expected_response_data, actual_response_data)

    def test_post_yes_decision_yes_match(self):
        """Does posting a "yes" decision that creates a match return the expected JSON?"""
        # Post a decision of Azura liking Boethiah:
        azura_decision_yes_data = {
            "user_id": self.azura_id,
            "candidate_id": self.boethiah_id,
            "outcome": True
        }
        self.db.post_decision(azura_decision_yes_data)

        # Post a second decision of Boethiah liking Azura
        boethiah_decision_yes_data = {
            "user_id": self.boethiah_id,
            "candidate_id": self.azura_id,
            "outcome": True
        }

        expected_response_data = {"match_created": True}

        actual_response_data = self.db.post_decision(
            boethiah_decision_yes_data)
        self.assertEqual(expected_response_data, actual_response_data)

    def test_post_no_decision(self):
        """Does posting a "no" decision return the expected JSON?"""
        decision_no_data = {
            "user_id": self.azura_id,
            "candidate_id": self.boethiah_id,
            "outcome": False
        }
        expected_response_data = {"match_created": False}
        actual_response_data = self.db.post_decision(decision_no_data)
        self.assertEqual(expected_response_data, actual_response_data)

    def test_non_boolean_outcome_raises_error(self):
        """Does posting JSON without a boolean outcome raise the expected error?"""
        bad_data = {
            "user_id": self.azura_id,
            "candidate_id": self.boethiah_id,
            "outcome": 2
        }
        with self.assertRaises(TypeError):
            self.db.post_decision(bad_data)

    ### Tests for get_datespots_near() ###

    # Code that makes live API calls isn't covered by the main tests suite

    def test_get_datespots_near_cache_only_default_radius(self):
        """Does the method return a string matching the expected shape of a JSON-ified list of Datespots
        in response to a query that provides valid location but no radius?"""
        location_query_data = {
            "location": (40.737291166191476, -74.00704685527774)
        }
        results = self.db.get_datespots_near(location_query_data)
        self.assertIsInstance(results, list)
        self.assertGreater(len(results), 0)
        first_result = results[0]
        distance, datespot = first_result  # it's a two-element tuple
        self.assertIsInstance(distance, float)
        self.assertIsInstance(datespot, models.Datespot)

    def test_get_datespots_near_cache_only_nondefault_radius(self):
        """Does the method return the expected JSON in response to a query that provides valid location
        and specifies a non-default_radius?"""
        location_query_data = {
            "location": (40.737291166191476, -74.00704685527774),
            "radius": 4000
        }
        results = self.db.get_datespots_near(location_query_data)
        self.assertIsInstance(results, list)
        self.assertGreater(len(results), 0)
        first_result = results[0]
        distance, datespot = first_result  # it's a two-element tuple
        self.assertIsInstance(distance, float)
        self.assertIsInstance(datespot, models.Datespot)

    ### Tests for get_datespot_suggestions() ###

    def test_get_candidate_datespots(self):
        """Does the method return the expected JSON in response to JSON matching with a valid Match?"""

        # Put a Match in the mock DB
        match_data = {"user1_id": self.azura_id, "user2_id": self.boethiah_id}
        match_id = self.db.post_object({
            "object_model_name": "match",
            "object_data": match_data
        })
        match_obj = self.match_data.lookup_obj(match_id)

        query_data = {"match_id": match_id}

        results = self.db.get_candidate_datespots(query_data)
        self.assertIsInstance(results, list)
        self.assertGreater(len(results), 0)
        # Terrezanos should be the only Datespot known to the DB here:
        self.assertEqual(results[0][1].id, self.terrezanos_id)

    ### Tests for other public methods ###
    def test_get_next_candidate(
        self
    ):  # We have two Users in the DB, so one will be the other's candidate
        query_data = {"user_id": self.azura_id}
        result = self.db.get_next_candidate(query_data)
        candidate_name = result["name"]
        self.assertEqual(self.boethiah_name, candidate_name)

    # TODO Post / get obj / get json for all of:
    #     user
    #     datespot
    #     match
    #     review
    #     message
    #     chat

    def test_get_matches_list(self):

        expected_result_data = [
            {  # Expecte the Match created second to appear first in the list
                "match_id":
                self.match_id_hircine_azura,
                "match_timestamp":
                self.match_obj_hircine_azura.timestamp,
                "match_partner_info":
                self.user_data.render_candidate(self.hircine_id)
            },
            {
                "match_id":
                self.match_id_azura_boethiah,
                "match_timestamp":
                self.match_obj_hircine_azura.timestamp,
                "match_partner_info":
                self.user_data.render_candidate(self.boethiah_id)
            }
        ]
        actual_result_data = self.db.get_matches_list(
            query_data={"user_id": self.azura_id})

        #self.assertEqual(actual_result_data, expected_result_data)
        # TODO Had to give up for now on asserting about the timestamps due to weird behavior--keep getting them created with identical timestamps
        #   when created in setUp, even though the timestamps increment in match.py.  Unittests for match.py confirmed that the underlying sort works.

        for result in actual_result_data:
            self.assertIsInstance(result, dict)
            self.assertEqual(len(result), len(expected_result_data[0]))

    def test_get_suggestions(self):

        expected_result_data = [  # Terrezanos is the only Datespot in the DB here
            self.datespot_data.render_obj(self.terrezanos_id)
        ]
        actual_result_data = self.db.get_suggestions_list(
            {"match_id": self.match_id_azura_boethiah})
        self.assertEqual(actual_result_data, expected_result_data)
예제 #2
0
class TestHelloWorldThings(unittest.TestCase):
    def setUp(self):

        self.db = DatabaseAPI()  # Testing on the real DB, to have restaurants
        # TODO: script that populates the test DBs with realistic restaurants en masse. And/or separate JSON map for this test
        #   (pointing to same test DB filenames for some like users, different one for datespots)
        self.user_data = model_interfaces.UserModelInterface()

        # Need user objects to instantiate a Match
        grortName = "Grort"
        self.grortCurrentLocation = (40.746667, -74.001111)
        grort_data = {
            "name": grortName,
            "current_location": self.grortCurrentLocation
        }
        self.grort_user_id = self.db.post_object({
            "object_model_name": "user",
            "object_data": grort_data
        })
        self.userGrort = self.user_data.lookup_obj(self.grort_user_id)

        drobbName = "Drobb"
        self.drobbCurrentLocation = (40.767376158866554, -73.98615327558278)
        drobb_data = {
            "name": drobbName,
            "current_location": self.drobbCurrentLocation
        }
        self.drobb_user_id = self.db.post_object({
            "object_model_name": "user",
            "object_data": drobb_data
        })
        self.userDrobb = self.user_data.lookup_obj(self.drobb_user_id)

        # distance should be approx 2610m
        # midpoint should be circa (40.75827478958617, -73.99310556132602)

        self.matchGrortDrobb = models.Match(self.userGrort, self.userDrobb)
        assert self.matchGrortDrobb.midpoint is not None

        # Get the candidates list that the DatabaseAPI would be giving to Match:
        self.candidate_datespots_list = self.db.get_datespots_near(
            {"location": self.matchGrortDrobb.midpoint})

    def test_hash(self):
        """Does the __hash__() method's return value match the value obtained by mimicking its logic in the test code?"""
        expected_hash = hash((self.grort_user_id, self.drobb_user_id))
        actual_hash = hash(self.matchGrortDrobb)
        self.assertEqual(actual_hash, expected_hash)

    def test_hash_output_consistent_regardless_of_user_order(self):
        """Does Match(Alice, Bob) hash to same value as Match(Bob, Alice)?"""
        expected_hash = hash(self.matchGrortDrobb)
        match_obj_flipped_members = models.Match(
            self.userDrobb, self.userGrort
        )  # Reverse the user1 and user2 roles from those in setUp
        assert match_obj_flipped_members.user1 == self.matchGrortDrobb.user2 and match_obj_flipped_members.user2 == self.matchGrortDrobb.user1
        actual_hash = hash(match_obj_flipped_members)
        self.assertEqual(actual_hash, expected_hash)

        # Test same for the public id property attribute
        expected_id = self.matchGrortDrobb.id
        actual_id = match_obj_flipped_members.id
        self.assertEqual(actual_id, expected_id)

        # TODO The implementation code isn't correct as of 6/10. Observed same user ID strings producing differnent Match id hashes
        #   during Postman endpoint testing.

    def test_public_id_attribute_matches_hash(self):
        """Does the public Match.id attribute-property bear the expected relationship to the return value Match.__hash__()?"""
        expected_id = str(hex(hash(self.matchGrortDrobb)))[
            2:]  # Mimic logic in Match._id() private method
        actual_id = self.matchGrortDrobb.id
        self.assertEqual(actual_id, expected_id)

    def test_compute_midpoint(self):
        maxDelta = 0.01
        approxExpectedMidpoint = (40.75827478958617, -73.99310556132602)
        expectedLat, expectedLon = approxExpectedMidpoint
        actualLat, actualLon = self.matchGrortDrobb.midpoint
        self.assertAlmostEqual(actualLat,
                               expectedLat,
                               delta=expectedLat * maxDelta)
        self.assertAlmostEqual(actualLon,
                               expectedLon,
                               delta=expectedLat * maxDelta)

    def test_public_distance_attribute(self):
        """Does the public distance attribute-property return the expected distance?"""
        expected_distance = geo_utils.haversine(self.grortCurrentLocation,
                                                self.drobbCurrentLocation)
        actual_distance = self.matchGrortDrobb.distance
        self.assertAlmostEqual(actual_distance, expected_distance)

    def test_get_suggestions_return_type(self):
        """Does Match.get_suggestions() external method return the expected type?"""
        expected_return_type = list
        returned_obj = self.matchGrortDrobb.suggestions(
            self.candidate_datespots_list)
        self.assertIsInstance(returned_obj, expected_return_type)

    def test_get_suggestions_return_not_null(self):
        returned_obj = self.matchGrortDrobb.suggestions(
            self.candidate_datespots_list)
        self.assertGreater(len(returned_obj), 0)

    def test_get_suggestions_return_shape(self):
        """Does the returned object's shape (nested lists/tuples) match the expected structure?"""
        returned_obj = self.matchGrortDrobb.suggestions(
            self.candidate_datespots_list)
        # each "suggestion" should be a Datespot object literal:
        for element in returned_obj:
            self.assertIsInstance(element, models.Datespot)

    # def test_db_user_method_returns_expected_query_results(self):
    #     """Does the method that calls the main database API return the expected query results, i.e.
    #     a list of serialized restaurant object dicts sorted by distance?"""
    #     returned_obj = self.matchGrortDrobb._get_datespots_by_geography()
    #     #print(returned_obj)
    #     self.assertIsInstance(returned_obj, list)
    #     self.assertGreater(len(returned_obj), 0)

    def test_internal_scorer_method_returns_expected_data(self):
        """Does the internal "private" method responsible for scoring each nearby datespot return a non-empty
        list?"""
        returned_obj = self.matchGrortDrobb._score_nearby_datespots(
            self.candidate_datespots_list)
        self.assertIsInstance(returned_obj, list)
        self.assertGreater(len(returned_obj), 0)