diff --git a/tjdests/apps/profile/tests.py b/tjdests/apps/profile/tests.py
new file mode 100644
index 0000000..b3dcc43
--- /dev/null
+++ b/tjdests/apps/profile/tests.py
@@ -0,0 +1,543 @@
+from django.urls import reverse
+
+from tjdests.apps.authentication.models import User
+from tjdests.apps.destinations.models import College, Decision, TestScore
+from tjdests.test import TJDestsTestCase
+
+
+class ProfileTest(TJDestsTestCase):
+    """Tests for the Profile app."""
+
+    def test_profile_view(self):
+        """Tests views.profile_view."""
+
+        # Test as an unauthenticated user.
+        response = self.client.get(reverse("profile:index"))
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            reverse("authentication:login") + f"?next={reverse('profile:index')}",
+            response.url,
+        )
+
+        # Test as a user that hasn't accepted TOS.
+        self.login(accept_tos=False, make_student=True)
+        response = self.client.get(reverse("profile:index"))
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(reverse("authentication:tos"), response.url)
+
+        # Test as a user that has accepted TOS
+        user = self.login(accept_tos=True, make_student=True)
+        response = self.client.get(reverse("profile:index"))
+        self.assertEqual(200, response.status_code)
+
+        # POST submit the profile form
+        response = self.client.post(
+            reverse("profile:index"),
+            data={
+                "biography": "hello",
+                "attending_decision": "",
+                "publish_data": False,
+            },
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            1,
+            User.objects.filter(
+                id=user.id,
+                biography="hello",
+                attending_decision=None,
+                publish_data=False,
+            ).count(),
+        )
+
+        # Test creating an admitted decision, then setting that as our destination.
+        college = College.objects.get_or_create(name="test college")[0]
+        decision = Decision.objects.get_or_create(
+            college=college, user=user, decision_type="ED", admission_status="ADMIT"
+        )[0]
+
+        response = self.client.post(
+            reverse("profile:index"),
+            data={
+                "biography": "hello2",
+                "attending_decision": decision.id,
+                "publish_data": True,
+            },
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            1,
+            User.objects.filter(
+                id=user.id,
+                biography="hello2",
+                attending_decision=decision,
+                publish_data=True,
+            ).count(),
+        )
+
+    def test_testscore_create(self):
+        """Tests creating test scores."""
+
+        # Load the page to create a test score
+        # First, test seniors only
+        self.login(accept_tos=True, make_student=True)
+        response = self.client.get(reverse("profile:testscores_add"))
+        self.assertEqual(403, response.status_code)
+
+        user = self.login(accept_tos=True, make_student=True, make_senior=True)
+        response = self.client.get(reverse("profile:testscores_add"))
+        self.assertEqual(200, response.status_code)
+
+        # POST and create a test score
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "SAT_TOTAL", "exam_score": 1600},
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            1,
+            TestScore.objects.filter(
+                user=user, exam_type="SAT_TOTAL", exam_score=1600
+            ).count(),
+        )
+
+        # Test invalid (and valid) test scores
+        # Invalid SAT > 1600
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "SAT_TOTAL", "exam_score": 1700},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                user=user, exam_type="SAT_TOTAL", exam_score=1700
+            ).count(),
+        )
+
+        # Invalid SAT, doesn't mod 10
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "SAT_TOTAL", "exam_score": 1543},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                user=user, exam_type="SAT_TOTAL", exam_score=1543
+            ).count(),
+        )
+
+        # Valid ACT
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "ACT_COMP", "exam_score": 5},
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            1,
+            TestScore.objects.filter(
+                user=user, exam_type="ACT_COMP", exam_score=5
+            ).count(),
+        )
+
+        # Invalid ACT, > 36
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "ACT_COMP", "exam_score": 37},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                user=user, exam_type="ACT_COMP", exam_score=37
+            ).count(),
+        )
+
+        # Invalid ACT, not an integer
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "ACT_COMP", "exam_score": 3.6},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                user=user, exam_type="ACT_COMP", exam_score=3.6
+            ).count(),
+        )
+
+        # Valid SAT2
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "SAT2_MATH2", "exam_score": 300},
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            1,
+            TestScore.objects.filter(
+                user=user, exam_type="SAT2_MATH2", exam_score=300
+            ).count(),
+        )
+
+        # Invalid SAT2, <200
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "SAT2_MATH2", "exam_score": 100},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                user=user, exam_type="SAT2_MATH2", exam_score=100
+            ).count(),
+        )
+
+        # Invalid SAT2, doesn't mod 10
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "SAT2_MATH2", "exam_score": 243},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                user=user, exam_type="SAT2_MATH2", exam_score=243
+            ).count(),
+        )
+
+        # Valid AP
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "AP_CSA", "exam_score": 5},
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            1,
+            TestScore.objects.filter(
+                user=user, exam_type="AP_CSA", exam_score=5
+            ).count(),
+        )
+
+        # Invalid AP, <1
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "AP_CSA", "exam_score": 0},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                user=user, exam_type="AP_CSA", exam_score=0
+            ).count(),
+        )
+
+        # Invalid AP, not an integer
+        response = self.client.post(
+            reverse("profile:testscores_add"),
+            data={"exam_type": "AP_CSA", "exam_score": 3.6},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                user=user, exam_type="AP_CSA", exam_score=3.6
+            ).count(),
+        )
+
+    def test_testscore_update(self):
+        """Tests the view to update testscores."""
+
+        user = self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Create a test score
+        testscore = TestScore.objects.get_or_create(
+            user=user, exam_type="AP_CSA", exam_score=5
+        )[0]
+
+        # Load the page to edit it
+        response = self.client.get(
+            reverse("profile:testscores_edit", kwargs={"pk": testscore.id})
+        )
+        self.assertEqual(200, response.status_code)
+
+        # Logging in as someone else should 404
+        self.login(
+            username="2021awilliam",
+            make_student=True,
+            make_senior=True,
+            accept_tos=True,
+        )
+        response = self.client.get(
+            reverse("profile:testscores_edit", kwargs={"pk": testscore.id})
+        )
+        self.assertEqual(404, response.status_code)
+
+        self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Change the score to a 4
+        response = self.client.post(
+            reverse("profile:testscores_edit", kwargs={"pk": testscore.id}),
+            data={"exam_type": "AP_CSA", "exam_score": 4},
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            1,
+            TestScore.objects.filter(
+                id=testscore.id, exam_type="AP_CSA", exam_score=4
+            ).count(),
+        )
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                id=testscore.id, exam_type="AP_CSA", exam_score=5
+            ).count(),
+        )
+
+        # Test invalid score
+        response = self.client.post(
+            reverse("profile:testscores_edit", kwargs={"pk": testscore.id}),
+            data={"exam_type": "AP_CSA", "exam_score": 6},
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            1,
+            TestScore.objects.filter(
+                id=testscore.id, exam_type="AP_CSA", exam_score=4
+            ).count(),
+        )
+        self.assertEqual(
+            0,
+            TestScore.objects.filter(
+                id=testscore.id, exam_type="AP_CSA", exam_score=6
+            ).count(),
+        )
+
+    def test_testscore_delete(self):
+        """Tests the view to delete testscores."""
+
+        user = self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Create a test score
+        testscore = TestScore.objects.get_or_create(
+            user=user, exam_type="AP_CSA", exam_score=5
+        )[0]
+
+        # Load the page to delete it
+        response = self.client.get(
+            reverse("profile:testscores_delete", kwargs={"pk": testscore.id})
+        )
+        self.assertEqual(200, response.status_code)
+
+        # Logging in as someone else should 404
+        self.login(
+            username="2021awilliam",
+            make_student=True,
+            make_senior=True,
+            accept_tos=True,
+        )
+        response = self.client.get(
+            reverse("profile:testscores_delete", kwargs={"pk": testscore.id})
+        )
+        self.assertEqual(404, response.status_code)
+
+        self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Delete it
+        response = self.client.post(
+            reverse("profile:testscores_delete", kwargs={"pk": testscore.id})
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(0, TestScore.objects.filter(id=testscore.id).count())
+
+    def test_decision_create(self):
+        """Tests the view to create decisions."""
+
+        user = self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Create a college.
+        college = College.objects.get_or_create(name="test college")[0]
+
+        # Load the page
+        response = self.client.get(reverse("profile:decision_add"))
+        self.assertEqual(200, response.status_code)
+
+        # Clear any decisions present
+        Decision.objects.all().delete()
+
+        # Add the decision
+        response = self.client.post(
+            reverse("profile:decision_add"),
+            data={
+                "college": college.id,
+                "decision_type": "RL",  # rolling
+                "admission_status": "ADMIT",
+            },
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            1,
+            Decision.objects.filter(
+                college=college,
+                user=user,
+                decision_type=Decision.ROLLING,
+                admission_status=Decision.ADMIT,
+            ).count(),
+        )
+
+        # Adding another decision of this college should not work
+        response = self.client.post(
+            reverse("profile:decision_add"),
+            data={
+                "college": college.id,
+                "decision_type": "RD",
+                "admission_status": "WAITLIST",
+            },
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(1, Decision.objects.filter(college=college, user=user).count())
+
+    def test_decision_update(self):
+        user = self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Create a college.
+        college = College.objects.get_or_create(name="test college")[0]
+
+        # Clear any decisions present
+        Decision.objects.all().delete()
+
+        # Create a decision
+        decision = Decision.objects.get_or_create(
+            college=college, user=user, decision_type="ED", admission_status="ADMIT"
+        )[0]
+
+        # Load the update page
+        response = self.client.get(
+            reverse("profile:decision_edit", kwargs={"pk": decision.id})
+        )
+        self.assertEqual(200, response.status_code)
+
+        # Logging in as someone else should 404
+        self.login(
+            username="2021awilliam",
+            make_student=True,
+            make_senior=True,
+            accept_tos=True,
+        )
+        response = self.client.get(
+            reverse("profile:decision_edit", kwargs={"pk": decision.id})
+        )
+        self.assertEqual(404, response.status_code)
+
+        self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Make an update but don't change the college
+        response = self.client.post(
+            reverse("profile:decision_edit", kwargs={"pk": decision.id}),
+            data={
+                "college": college.id,
+                "decision_type": "ED",
+                "admission_status": "DENY",
+            },
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            1,
+            Decision.objects.filter(
+                college=college,
+                user=user,
+                decision_type="ED",
+                admission_status="DENY",
+                id=decision.id,
+            ).count(),
+        )
+        self.assertEqual(
+            0,
+            Decision.objects.filter(
+                college=college, user=user, decision_type="ED", admission_status="ADMIT"
+            ).count(),
+        )
+
+        # Change the college
+        college2 = College.objects.get_or_create(name="University of Test")[0]
+        response = self.client.post(
+            reverse("profile:decision_edit", kwargs={"pk": decision.id}),
+            data={
+                "college": college2.id,
+                "decision_type": "ED",
+                "admission_status": "DENY",
+            },
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(
+            1,
+            Decision.objects.filter(
+                college=college2,
+                user=user,
+                decision_type="ED",
+                admission_status="DENY",
+                id=decision.id,
+            ).count(),
+        )
+        self.assertEqual(0, Decision.objects.filter(college=college).count())
+
+        # Add a decision for college (not college2)
+        Decision.objects.get_or_create(
+            college=college, user=user, decision_type="ED", admission_status="ADMIT"
+        )
+
+        # Now, there are two decisions, one for college and one for college2
+        # Try to edit the decision for college2 -> college
+        decision2 = Decision.objects.get(college=college2, user=user)
+
+        response = self.client.post(
+            reverse("profile:decision_edit", kwargs={"pk": decision2.id}),
+            data={
+                "college": college.id,
+                "decision_type": "ED",
+                "admission_status": "DENY",
+            },
+        )
+        self.assertEqual(200, response.status_code)
+        self.assertEqual(
+            1, Decision.objects.filter(college=college2, user=user).count()
+        )
+        self.assertEqual(1, Decision.objects.filter(college=college, user=user).count())
+
+    def test_decision_delete(self):
+        """Tests the view to delete a decision."""
+
+        user = self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Create a decision
+        college = College.objects.get_or_create(name="test college")[0]
+        decision = Decision.objects.get_or_create(
+            college=college, user=user, decision_type="ED", admission_status="ADMIT"
+        )[0]
+
+        # Load the update page
+        response = self.client.get(
+            reverse("profile:decision_delete", kwargs={"pk": decision.id})
+        )
+        self.assertEqual(200, response.status_code)
+
+        # Logging in as someone else should 404
+        self.login(
+            username="2021awilliam",
+            make_student=True,
+            make_senior=True,
+            accept_tos=True,
+        )
+        response = self.client.get(
+            reverse("profile:decision_delete", kwargs={"pk": decision.id})
+        )
+        self.assertEqual(404, response.status_code)
+
+        self.login(make_senior=True, make_student=True, accept_tos=True)
+
+        # Delete the decision
+        response = self.client.post(
+            reverse("profile:decision_delete", kwargs={"pk": decision.id})
+        )
+        self.assertEqual(302, response.status_code)
+        self.assertEqual(0, Decision.objects.filter(id=decision.id).count())
diff --git a/tjdests/test.py b/tjdests/test.py
new file mode 100644
index 0000000..d4a669c
--- /dev/null
+++ b/tjdests/test.py
@@ -0,0 +1,38 @@
+from django.test import TestCase
+
+from tjdests.apps.authentication.models import User
+
+
+class TJDestsTestCase(TestCase):
+    def login(  # pylint: disable=too-many-arguments
+        self,
+        username: str = "awilliam",
+        accept_tos: bool = False,
+        make_student: bool = False,
+        make_senior: bool = False,
+        make_superuser: bool = False,
+    ) -> User:
+        """
+        Log in as the specified user.
+
+        Args:
+            username: The username to log in as.
+            accept_tos: Whether to accept the terms or not.
+            make_student: Whether to make this user a student.
+            make_senior: Whether to make this user a senior.
+            make_superuser: Whether to make this user a superuser.
+        Return:
+            The user.
+        """
+        user = User.objects.update_or_create(
+            username=username,
+            defaults={
+                "is_student": make_student,
+                "is_staff": make_superuser,
+                "is_superuser": make_superuser,
+                "is_senior": make_senior,
+                "accepted_terms": accept_tos,
+            },
+        )[0]
+        self.client.force_login(user)
+        return user