Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
df80391
initial commit
bwang-icf Nov 6, 2025
cfeb712
adding version specific permission checking so v3 resources are recog…
bwang-icf Nov 6, 2025
ef64ba6
TODO in authorization/views.py. Need to write unit tests for new util…
JamesDemeryNava Nov 6, 2025
cf33395
Address TODOs in bb2_tools/admin.py
JamesDemeryNava Nov 6, 2025
ab29772
found request.path to contain version. Different from other places bu…
bwang-icf Nov 6, 2025
433d5c5
Ensure fhir_id v2 or v3 are updated if none when re-authorizing. v3 t…
JamesDemeryNava Nov 6, 2025
157d988
adding extra validation for incorrect versions in utils and using it …
bwang-icf Nov 6, 2025
2707686
V2 calls all working, v3 patient returning correct data but built in …
JamesDemeryNava Nov 7, 2025
6cae063
adding more generalized version grabbing for crosswalk permissions
bwang-icf Nov 7, 2025
a58570a
Revert log to reduce test failures (will revisit this log once all fu…
JamesDemeryNava Nov 7, 2025
6bf69b0
Ensure v3 coverage and EOD endpoints work. Currently 7 test failures …
JamesDemeryNava Nov 7, 2025
cb698da
changes to get_patient_id, test_read_and_search and other tests to ex…
bwang-icf Nov 7, 2025
4838058
Modify some tests, pass a version to generate_info_headers. Tests are…
JamesDemeryNava Nov 7, 2025
8614644
Merge branch 'master' into bw/BB2-4166
JamesDemeryNava Nov 7, 2025
c65b63c
added notes and some changes to mymedicare views
bwang-icf Nov 7, 2025
90c7921
Interim collab commit
jadudm Nov 10, 2025
672c806
Remove completed TODOs
JamesDemeryNava Nov 10, 2025
87c39e7
Remove more completed TODOs
JamesDemeryNava Nov 10, 2025
717462b
Remove more TODOs that had been completed
JamesDemeryNava Nov 10, 2025
ebf5125
Fix audit_logger tests, WIP on authentication
JamesDemeryNava Nov 10, 2025
8525ea3
Fixed mocked tests
jadudm Nov 10, 2025
8ea6bd7
Removing print statements
jadudm Nov 10, 2025
6aec825
Merge branch 'master' into bw/BB2-4166
jadudm Nov 10, 2025
e6bc40b
Add version param to create_fhir_mock to help tests succeed
JamesDemeryNava Nov 10, 2025
aa7dc60
Revert unneeded test changes, ensure still passes
JamesDemeryNava Nov 10, 2025
f4c043d
Fix last failing test in test_authentication.py
JamesDemeryNava Nov 10, 2025
69e9d8d
Add unit test, remove completed TODOs
JamesDemeryNava Nov 12, 2025
dd315f8
WIP On fixing tests
JamesDemeryNava Nov 12, 2025
98c523b
Interim
jadudm Nov 12, 2025
b873afb
Fixed callback tests
jadudm Nov 12, 2025
c180fd1
fix for test_callback_allow_slsx_changes_to_hicn_and_mbi
JamesDemeryNava Nov 12, 2025
f280493
Revert responses.py changes
JamesDemeryNava Nov 12, 2025
2da17de
Fixed import
jadudm Nov 12, 2025
d97fd9b
Renaming.
jadudm Nov 13, 2025
5b43959
Rename function, modify return type per PR feedback
JamesDemeryNava Nov 13, 2025
173ec43
Modify imports to reflect new file name
JamesDemeryNava Nov 13, 2025
c411169
Modify default param and update to absolute paths
JamesDemeryNava Nov 13, 2025
56928af
Remove unneeded TODO
JamesDemeryNava Nov 13, 2025
1c1732c
Modify a function name and call to more accurately reflect what it does
JamesDemeryNava Nov 13, 2025
3982daa
revert to have v2 as default to fhir_id, as it broke tests, and we on…
JamesDemeryNava Nov 13, 2025
0bdd43d
Check on blank string in get_and_update_user to accommodate changes f…
JamesDemeryNava Nov 13, 2025
b95785b
Single quotes and comment context
JamesDemeryNava Nov 13, 2025
f6d4d7d
Refactor get_and_update_user to reduce BFD calls, and to ensure fhir_…
JamesDemeryNava Nov 13, 2025
1ea136e
Updates based on PR self-review with Brandon and Matt
JamesDemeryNava Nov 13, 2025
85fd19c
adding latest versions and changes to the offset math test
bwang-icf Nov 13, 2025
675f69d
added defaults if there is no fhir_id found for a version
bwang-icf Nov 14, 2025
72f0819
Merge branch 'master' into bw/BB2-4166
bwang-icf Nov 14, 2025
a31aeba
Merge branch 'master' into bw/BB2-4166
bwang-icf Nov 14, 2025
f19d8a4
addressing comments
bwang-icf Nov 17, 2025
4cf4355
Merge branch 'master' into bw/BB2-4166
JamesDemeryNava Nov 18, 2025
4e41897
Merge branch 'master' into bw/BB2-4166
clewellyn-nava Nov 19, 2025
63e5245
Resolve conflict, still work to do to resolve TODO
JamesDemeryNava Nov 19, 2025
80307e3
changing check to empty string over None but will change back to None…
bwang-icf Nov 19, 2025
6021985
Address TODO that arose from a change made in BB2-4233
JamesDemeryNava Nov 19, 2025
91c3a37
use self.version instead
JamesDemeryNava Nov 19, 2025
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
4 changes: 1 addition & 3 deletions apps/accounts/views/oauth2_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from apps.fhir.bluebutton.models import Crosswalk
from apps.fhir.bluebutton.permissions import ApplicationActivePermission

from apps.constants import Versions
from apps.versions import Versions


def _get_userinfo(user, version=Versions.NOT_AN_API_VERSION):
Expand Down Expand Up @@ -46,8 +46,6 @@ def _get_userinfo(user, version=Versions.NOT_AN_API_VERSION):
def _openidconnect_userinfo(request, version=Versions.NOT_AN_API_VERSION):
# NOTE: The **kwargs are not used anywhere down the callchain, and are being ignored.

# BB2-4166-TODO: will the request have a version? do we get here from redirects or is this
# a straight url that we need to get the version from the url (like we do in the fhir app)
return JsonResponse(_get_userinfo(request.resource_owner, version))


Expand Down
7 changes: 5 additions & 2 deletions apps/authorization/permissions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.conf import settings
from rest_framework import (permissions, exceptions)
from apps.versions import Versions, VersionNotMatched

from .models import DataAccessGrant

Expand Down Expand Up @@ -32,8 +33,10 @@ def has_object_permission(self, request, view, obj):
# Patient resources were taken care of above
# Return 404 on error to avoid notifying unauthorized user the object exists

# BB2-4166-TODO: this is hardcoded to be version 2
return is_resource_for_patient(obj, request.crosswalk.fhir_id(2))
if view.version in Versions.supported_versions():
return is_resource_for_patient(obj, request.crosswalk.fhir_id(view.version))
else:
raise VersionNotMatched()


def is_resource_for_patient(obj, patient_id):
Expand Down
22 changes: 17 additions & 5 deletions apps/authorization/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
from oauth2_provider.views.base import OAuthLibMixin
from oauth2_provider.views.generic import ClientProtectedResourceView

from apps.versions import VersionNotMatched, Versions
from apps.dot_ext.authentication import SLSAuthentication
from .models import DataAccessGrant
from ..dot_ext.utils import get_application_from_meta
from ..fhir.bluebutton.models import Crosswalk
from apps.authorization.models import DataAccessGrant
from apps.dot_ext.utils import get_application_from_meta, get_api_version_number_from_url
from apps.fhir.bluebutton.models import Crosswalk


Application = get_application_model()
Expand Down Expand Up @@ -68,9 +69,20 @@ class ExpireDataAccessGrantView(ClientProtectedResourceView, OAuthLibMixin):
@staticmethod
def post(request, *args, **kwargs):
try:
path_info = request.__dict__.get('path_info')
version = get_api_version_number_from_url(path_info)
patient_id = kwargs.pop('patient_id', None)
# BB2-4166-TODO: currently hardcoded for v2, might need to not be static
user = Crosswalk.objects.get(fhir_id_v2=patient_id).user

match version:
case Versions.V1:
user = Crosswalk.objects.get(fhir_id_v2=patient_id).user
Comment on lines +78 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have fhir_id_v1 or both v1 and v2 share fhir_id_v2 , if so could you add comment in here to avoid confusion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure thing. I can add a comment here, but for most areas we are definitely treating v1/v2 as the same thing since their pathways are fairly similar.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stiwarisemanticbits , for reference (and so we have it here):

There used to be only fhir_id. We have now introduced _v2 and _v3, and the references to fhir_id became a function on the model that now takes a version parameter. So, everywhere we used to say:

crosswalk.fhir_id

(which was, at that time, implicitly already v1 and v2)

we now say:

crosswalk.fhir_id(Version.Vx)

So, commenting that V1 now shares V2 is something we would have to comment throughout the entire codebase. @bwang-icf , before we start commenting this in the code, we may want to decide if/where we need to document this kind of design work. I personally think it would be the TRD.

Copy link
Contributor

@clewellyn-nava clewellyn-nava Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think commenting on the model is sufficient, otherwise we run into issues you described.

I believe we can also use | in case statements in Python 3.11
case Version.V1 | Version.V2
This has the code read that they are treated the same, and if someone is curious as to why, they can reference the model comment. Just my two cents.

case Versions.V2:
user = Crosswalk.objects.get(fhir_id_v2=patient_id).user
case Versions.V3:
user = Crosswalk.objects.get(fhir_id_v3=patient_id).user
case _:
raise VersionNotMatched(f"{version} is not a valid version constant")

client = get_application_from_meta(request)
DataAccessGrant.objects.get(beneficiary=user.id, application=client).delete()
except Crosswalk.DoesNotExist:
Expand Down
15 changes: 6 additions & 9 deletions apps/bb2_tools/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
DummyAdminObject,
UserStats,
)
from apps.fhir.bluebutton.utils import get_patient_by_id
from apps.fhir.bluebutton.utils import get_v2_patient_by_id

ADMIN_PREPEND = getattr(settings, "ADMIN_PREPEND_URL", "")
BB2_TOOLS_PATH = (
Expand Down Expand Up @@ -332,8 +332,7 @@ class BeneficiaryDashboardAdmin(ReadOnlyAdmin):
"get_connected_applications",
"date_created",
)
# BB2-4166-TODO: add support for v3
search_fields = ('user__username', 'fhir_id_v2', '_user_id_hash', '_user_mbi')
search_fields = ('user__username', 'fhir_id_v2', 'fhir_id_v3', '_user_id_hash', '_user_mbi')
readonly_fields = ('date_created',)
raw_id_fields = ('user',)

Expand Down Expand Up @@ -361,10 +360,9 @@ def get_access_tokens(self, obj):
ordering="MyIdentities",
)
def get_identities(self, obj):
# BB2-4166-TODO: add support for v3
return format_html(
'<div><ul><li>FHIR_ID_V2:{}</li><li>HICN HASH:{}</li><li>MBI:{}</li>'.format(
obj.fhir_id(2), obj.user_hicn_hash, obj.user_mbi
'<div><ul><li>FHIR_ID_V2:{}</li><li>FHIR_ID_V3:{}</li><li>HICN HASH:{}</li><li>MBI:{}</li>'.format(
obj.fhir_id(2), obj.fhir_id(3), obj.user_hicn_hash, obj.user_mbi
)
)

Expand Down Expand Up @@ -416,10 +414,9 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
crosswalk = BeneficiaryDashboard.objects.get(pk=int(object_id))

json_resp = None

try:
# BB2-4166-TODO: this is hardcoded to be version 2
json_resp = get_patient_by_id(crosswalk.fhir_id(2), request)
# DEPRECATE_V2: If we ever deprecate v2, this function and call need to be updated
json_resp = get_v2_patient_by_id(crosswalk.fhir_id(2), request)
except Exception as e:
json_resp = {"backend_error": str(e)}

Expand Down
5 changes: 3 additions & 2 deletions apps/dot_ext/oauth2_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from ..fhir.bluebutton.models import Crosswalk
from .loggers import (clear_session_auth_flow_trace, update_session_auth_flow_trace_from_code,
set_session_auth_flow_trace_value)
from apps.dot_ext.utils import get_api_version_number_from_url


class OAuthLibSMARTonFHIR(OAuthLibCore):
Expand Down Expand Up @@ -31,8 +32,8 @@ def create_token_response(self, request):
if Crosswalk.objects.filter(user=token.user).exists():
fhir_body = json.loads(body)
cw = Crosswalk.objects.get(user=token.user)
# BB2-4166-TODO: this is hardcoded to be version 2
fhir_body["patient"] = cw.fhir_id(2)
version = get_api_version_number_from_url(request.path)
fhir_body['patient'] = cw.fhir_id(version)
body = json.dumps(fhir_body)

return uri, headers, body, status
2 changes: 1 addition & 1 deletion apps/dot_ext/tests/test_api_error_codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from apps.authorization.models import (
DataAccessGrant,
)
from apps.constants import AccessType
from apps.versions import AccessType
from datetime import datetime
from dateutil.relativedelta import relativedelta
from unittest import mock
Expand Down
24 changes: 24 additions & 0 deletions apps/dot_ext/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from django.test import TestCase

from apps.versions import VersionNotMatched
from ..utils import get_api_version_number_from_url

SUPPORTED_VERSION_TEST_CASES = [
{'url_path': '/v2/fhir/Patient/', 'expected': 2},
# return 0 because v2 does not have a leading /
{'url_path': 'v2/fhir/Patient/', 'expected': 0},
{'url_path': '/v3/fhir/Coverage/', 'expected': 3},
{'url_path': '/v3/fhir/Coverage/v2/', 'expected': 3},
]


class TestDOTUtils(TestCase):
def test_get_api_version_number(self):
for test in SUPPORTED_VERSION_TEST_CASES:
result = get_api_version_number_from_url(test['url_path'])
assert result == test['expected']

def test_get_api_version_number_unsupported_version(self):
# unsupported version will raise an exception
with self.assertRaises(VersionNotMatched, msg='4 extracted from /v4/fhir/Coverage/'):
get_api_version_number_from_url('/v4/fhir/Coverage/')
25 changes: 25 additions & 0 deletions apps/dot_ext/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from oauth2_provider.models import AccessToken, RefreshToken, get_application_model
from oauthlib.oauth2.rfc6749.errors import InvalidClientError, InvalidGrantError, InvalidRequestError
from http import HTTPStatus
import re
from apps.versions import Versions, VersionNotMatched

from apps.authorization.models import DataAccessGrant

Expand Down Expand Up @@ -252,3 +254,26 @@ def json_response_from_oauth2_error(error):
ret_data['error_description'] = error.description

return JsonResponse(ret_data, status=error.status_code)


def get_api_version_number_from_url(url_path: str) -> int:
"""Utility function to extract what version of the API a URL is
If there are multiple occurrences of 'v{{VERSION}} in a url path,
only return the first one
EX. /v2/o/authorize will return v2.

Args:
url_path (str): The url being called that we want to extract the api version

Returns:
Optional[str]: Returns a string of v2
"""
match = re.search(r'/v(\d+)/', url_path, re.IGNORECASE)
if match:
version = int(match.group(1))
if version in Versions.supported_versions():
return version
else:
raise VersionNotMatched(f'{version} extracted from {url_path}')

return Versions.NOT_AN_API_VERSION
22 changes: 10 additions & 12 deletions apps/fhir/bluebutton/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from django.core.validators import MinLengthValidator
from apps.accounts.models import get_user_id_salt

from apps.versions import Versions, VersionNotMatched


class BBFhirBluebuttonModelException(APIException):
# BB2-237 custom exception
Expand Down Expand Up @@ -164,29 +166,25 @@ class Meta:
)
]

def fhir_id(self, version: int = 2) -> str:
def fhir_id(self, version: int = Versions.V2) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible we want to remove the default here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we might do Versions.NOT_AN_API_VERSION as a default. 🤔

We'll discuss during our walkthrough. Good thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would definitely be ideal but changing this default led to a lot of errors within the apps/logging/serializer and that is possibly tied in with the fact that we would no longer have a fhir_id column. We'll create a ticket to address this more properly and will be part of a deprecate v2 workflow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"""Helper method to return fhir_id based on BFD version, preferred over direct access"""
if version in (1, 2):
if version in [Versions.V1, Versions.V2]:
if self.fhir_id_v2 is not None and self.fhir_id_v2 != '':
# TODO - This is legacy code, to be removed before migration bluebutton 0010
# If fhir_id is empty, try to populate it from fhir_id_v2 to support old code
self._fhir_id = self.fhir_id_v2
self.save()
return str(self.fhir_id_v2)
return self.fhir_id_v2
# TODO - This is legacy code, to be removed before migration bluebutton 0010
# If fhir_id_v2 is empty, try to populate it from _fhir_id to support new code
if self._fhir_id is not None and self._fhir_id != '':
self.fhir_id_v2 = self._fhir_id
self.save()
return str(self._fhir_id)
return self._fhir_id
return ''
elif version == 3:
# TODO BB2-4166: This will want to change. In order to make
# BB2-4181 work, the V3 value needed to be found in the V2 column.
# 4166 should flip this to _v3, and we should be able to find
# values there when using (say) the test client.
if self.fhir_id_v2 is not None and self.fhir_id_v2 != '':
return str(self.fhir_id_v2)
elif version == Versions.V3:
if self.fhir_id_v3 is not None and self.fhir_id_v3 != '':
return self.fhir_id_v3
return ''
else:
raise ValidationError(f'{version} is not a valid BFD version')
Expand All @@ -203,7 +201,7 @@ def set_fhir_id(self, value, version: int = 2) -> None:
elif version == 3:
self.fhir_id_v3 = value
else:
raise ValidationError(f'{version} is not a valid BFD version')
raise VersionNotMatched(f'{version} is not a valid BFD version')

@property
def user_hicn_hash(self):
Expand Down
31 changes: 17 additions & 14 deletions apps/fhir/bluebutton/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from rest_framework import permissions, exceptions
from rest_framework.exceptions import AuthenticationFailed
from .constants import ALLOWED_RESOURCE_TYPES
from apps.versions import Versions, VersionNotMatched

import apps.logging.request_logger as bb2logging

Expand All @@ -30,35 +31,36 @@ def has_permission(self, request, view):

class HasCrosswalk(permissions.BasePermission):
def has_permission(self, request, view):
return bool(
# BB2-4166-TODO : this needs to use version to determine fhir_id, probably in request
request.user and request.user.crosswalk and request.user.crosswalk.fhir_id(2)
)
if view.version in Versions.supported_versions():
return request.user and request.user.crosswalk and request.user.crosswalk.fhir_id(view.version)
else:
# this should not happen where we'd get an unsupported version
raise VersionNotMatched("Version not matched in has_permission")


class ReadCrosswalkPermission(HasCrosswalk):
def has_object_permission(self, request, view, obj):
# Now check that the user has permission to access the data
# Patient resources were taken care of above # TODO - verify this
# Return 404 on error to avoid notifying unauthorized user the object exists

if view.version in Versions.supported_versions():
fhir_id = request.crosswalk.fhir_id(view.version)
else:
raise VersionNotMatched("Version not matched in has_object_permission in ReadCrosswalkPermission")
try:
if request.resource_type == "Coverage":
reference = obj["beneficiary"]["reference"]
reference_id = reference.split("/")[1]
# BB2-4166-TODO : this needs to use version to determine fhir_id, probably in request
if reference_id != request.crosswalk.fhir_id(2):
if reference_id != fhir_id:
raise exceptions.NotFound()
elif request.resource_type == "ExplanationOfBenefit":
reference = obj["patient"]["reference"]
reference_id = reference.split("/")[1]
# BB2-4166-TODO : this needs to use version to determine fhir_id, probably in request
if reference_id != request.crosswalk.fhir_id(2):
if reference_id != fhir_id:
raise exceptions.NotFound()
else:
reference_id = obj["id"]
# BB2-4166-TODO : this needs to use version to determine fhir_id, probably in request
if reference_id != request.crosswalk.fhir_id(2):
if reference_id != fhir_id:
raise exceptions.NotFound()

except exceptions.NotFound:
Expand All @@ -71,9 +73,10 @@ def has_object_permission(self, request, view, obj):

class SearchCrosswalkPermission(HasCrosswalk):
def has_object_permission(self, request, view, obj):
# BB2-4166-TODO: this is hardcoded to be version 2
patient_id = request.crosswalk.fhir_id(2)

if view.version in Versions.supported_versions():
patient_id = request.crosswalk.fhir_id(view.version)
else:
raise VersionNotMatched("Version not matched in has_object_permission in SearchCrosswalkPermission")
if "patient" in request.GET and request.GET["patient"] != patient_id:
return False

Expand Down
40 changes: 17 additions & 23 deletions apps/fhir/bluebutton/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,23 @@ def test_not_require_user_mbi(self):
cw = Crosswalk.objects.get(user=user)
self.assertEqual(cw.user_mbi, None)

# TODO BB2-4166: This was commented out as part of
# BB2-4181, but after the work of 4166 is done, this test should
# pass. However, in 4181, we are storing _v3 values in _v2 (at the model level), and
# therefore the logic of this test does not work in 4181's branch.
# Once 4166 is fully implemented, uncomment this test, and it should pass and
# navigating the test client using the v3 pathway should work.
# def test_mutable_fhir_id(self):
# user = self._create_user(
# "john",
# "password",
# first_name="John",
# last_name="Smith",
# email="[email protected]",
# fhir_id_v2="-20000000000001",
# fhir_id_v3="-30000000000001",
# )
# cw = Crosswalk.objects.get(user=user)
# self.assertEqual(cw.fhir_id(2), "-20000000000001")
# self.assertEqual(cw.fhir_id(3), "-30000000000001")
# cw.set_fhir_id("-20000000000002", 2)
# cw.set_fhir_id("-30000000000002", 3)
# self.assertEqual(cw.fhir_id(2), "-20000000000002")
# self.assertEqual(cw.fhir_id(3), "-30000000000002")
def test_mutable_fhir_id(self):
user = self._create_user(
"john",
"password",
first_name="John",
last_name="Smith",
email="[email protected]",
fhir_id_v2="-20000000000001",
fhir_id_v3="-30000000000001",
)
cw = Crosswalk.objects.get(user=user)
self.assertEqual(cw.fhir_id(2), "-20000000000001")
self.assertEqual(cw.fhir_id(3), "-30000000000001")
cw.set_fhir_id("-20000000000002", 2)
cw.set_fhir_id("-30000000000002", 3)
self.assertEqual(cw.fhir_id(2), "-20000000000002")
self.assertEqual(cw.fhir_id(3), "-30000000000002")

def test_mutable_user_hicn_hash(self):
user = self._create_user(
Expand Down
Loading
Loading