Skip to content

Commit

Permalink
Feature/multi use invitation (#227)
Browse files Browse the repository at this point in the history
* MultiUse or multi_use

Signed-off-by: Jason Sy <[email protected]>

* didn't save some

Signed-off-by: Jason Sy <[email protected]>

* column rename migrations

Signed-off-by: Jason Sy <[email protected]>

* multi-use bdd test

Signed-off-by: Jason Sy <[email protected]>

* test

Signed-off-by: Jason Sy <[email protected]>

* lint fix

Signed-off-by: Jason Sherman <[email protected]>

* we need to not tox lint these files

Signed-off-by: Jason Sy <[email protected]>

Co-authored-by: usingtechnology <[email protected]>
Co-authored-by: Jason Sherman <[email protected]>
  • Loading branch information
3 people authored Jul 21, 2022
1 parent fc82073 commit e0fd1d5
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""rename reusable to multi_use
Revision ID: 5f13cfb97471
Revises: cd86ad6d3331
Create Date: 2022-07-21 18:27:12.052800
"""
from alembic import op


# revision identifiers, used by Alembic.
revision = "5f13cfb97471"
down_revision = "cd86ad6d3331"
branch_labels = None
depends_on = None


def upgrade():
op.alter_column("connection_invitation", "reusable", new_column_name="multi_use")


def downgrade():
op.alter_column("connection_invitation", "multi_use", new_column_name="reusable")
4 changes: 2 additions & 2 deletions services/traction/api/db/models/v1/connection_invitation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class ConnectionInvitation(TenantScopedModel, table=True):
name: Label or Name for the Invitation
status: Business and Tenant indicator for Invitation status
tags: Set by tenant for adding to resulting Contacts that used this invitation
reusable: When true, this invitation is reusable
multi_use: When true, this invitation is multi_use
public: When true, this invitation is public
deleted: Invitations "soft" delete indicator.
connection_id: Underlying AcaPy connection id
Expand All @@ -57,7 +57,7 @@ class ConnectionInvitation(TenantScopedModel, table=True):
name: str = Field(nullable=False, index=True)
status: str = Field(nullable=False)
state: str = Field(nullable=False)
reusable: bool = Field(nullable=False, default=False)
multi_use: bool = Field(nullable=False, default=False)
public: bool = Field(nullable=False, default=False)

tags: List[str] = Field(sa_column=Column(ARRAY(String)))
Expand Down
20 changes: 10 additions & 10 deletions services/traction/api/endpoints/models/v1/invitations.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ class InvitationItem(
Attributes:
name: Name for the Invitation, used to find and add context to its purpose
reusable: when True, this is a reusable invitation
multi_use: when True, this is a multi_use invitation
public: when True, this is a public invitation
invitation_url: URL for consumers to "receive"
"""

name: str
reusable: bool | None = False
multi_use: bool | None = False
public: bool | None = False
invitation_url: str

Expand All @@ -73,7 +73,7 @@ class InvitationListParameters(
Attributes:
name: return InvitationItems like name
reusable: when True, return InvitationItems marked reusable
multi_use: when True, return InvitationItems marked multi_use
public: when True, return InvitationItems marked public
deleted: when True, return InvitationItems that are deleted
Expand All @@ -91,10 +91,10 @@ class InvitationGetResponse(GetResponse[InvitationItem]):
pass


class CreateReusableInvitationPayload(BaseModel):
"""CreateReusableInvitationPayload.
class CreateMultiUseInvitationPayload(BaseModel):
"""CreateMultiUseInvitationPayload.
Payload for Create Reusable Invitation API.
Payload for Create MultiUse Invitation API.
Attributes:
name: required, must be unique. A name/label for the Invitation
Expand All @@ -108,13 +108,13 @@ class CreateReusableInvitationPayload(BaseModel):
tags: Optional[List[str]] | None = None


class CreateReusableInvitationResponse(GetResponse[InvitationItem]):
"""CreateReusableInvitationResponse.
class CreateMultiUseInvitationResponse(GetResponse[InvitationItem]):
"""CreateMultiUseInvitationResponse.
Response to Create Reusable Invitation API.
Response to Create MultiUse Invitation API.
Attributes:
invitation_url: url to the reusable invitation.
invitation_url: url to the MultiUse invitation.
"""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from api.endpoints.models.v1.invitations import (
InvitationListResponse,
InvitationListParameters,
CreateReusableInvitationResponse,
CreateReusableInvitationPayload,
CreateMultiUseInvitationResponse,
CreateMultiUseInvitationPayload,
)
from api.services.v1 import invitations_service

Expand Down Expand Up @@ -55,20 +55,20 @@ async def list_invitations(


@router.post(
"/create-reusable-invitation",
"/create-multi-use-invitation",
status_code=status.HTTP_200_OK,
response_model=CreateReusableInvitationResponse,
response_model=CreateMultiUseInvitationResponse,
)
async def create_reusable_invitation(
payload: CreateReusableInvitationPayload,
async def create_multi_use_invitation(
payload: CreateMultiUseInvitationPayload,
db: AsyncSession = Depends(get_db),
) -> CreateReusableInvitationResponse:
) -> CreateMultiUseInvitationResponse:
wallet_id = get_from_context("TENANT_WALLET_ID")
tenant_id = get_from_context("TENANT_ID")
(item, invitation_url,) = await invitations_service.create_reusable_invitation(
(item, invitation_url,) = await invitations_service.create_multi_use_invitation(
db, tenant_id, wallet_id, payload=payload
)
links = [] # TODO
return CreateReusableInvitationResponse(
return CreateMultiUseInvitationResponse(
item=item, invitation_url=invitation_url, links=links
)
6 changes: 3 additions & 3 deletions services/traction/api/protocols/v1/connection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
ConnectionNotifier,
)
from api.protocols.v1.connection.contact_status_updater import ContactStatusUpdater
from api.protocols.v1.connection.reusable_invitation_processor import (
ReusableInvitationProcessor,
from api.protocols.v1.connection.multi_use_invitation_processor import (
MultiUseInvitationProcessor,
)


def subscribe_connection_protocol_listeners():
ReusableInvitationProcessor()
MultiUseInvitationProcessor()
ContactStatusUpdater()
ConnectionNotifier()
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from api.protocols.v1.connection.connection_protocol import DefaultConnectionProtocol


async def is_reusable_invitation(
async def is_multi_use_invitation(
profile: Profile, payload: dict
) -> ConnectionInvitation:
result = None
Expand All @@ -18,22 +18,22 @@ async def is_reusable_invitation(
invitation = await ConnectionInvitation.get_by_invitation_key(
db, profile.tenant_id, payload["invitation_key"]
)
if invitation and invitation.reusable:
if invitation and invitation.multi_use:
result = invitation

return result


class ReusableInvitationProcessor(DefaultConnectionProtocol):
class MultiUseInvitationProcessor(DefaultConnectionProtocol):
def __init__(self):
super().__init__(role=ConnectionRoleType.inviter)

async def on_request(self, profile: Profile, payload: dict):
self.logger.info("> on_request()")
invitation = await is_reusable_invitation(profile, payload)
invitation = await is_multi_use_invitation(profile, payload)
if invitation:
self.logger.debug(
f"on_request >> reusable invitation ({invitation.invitation_key})"
f"on_request >> multi_use invitation ({invitation.invitation_key})"
)
# create a contact, we will update the alias when we reach active
label = payload["invitation_key"]
Expand All @@ -52,15 +52,15 @@ async def on_request(self, profile: Profile, payload: dict):
db.add(db_contact)
await db.commit()
else:
self.logger.debug("on_request >> not a reusable invitation")
self.logger.debug("on_request >> not a multi_use invitation")
self.logger.info("< on_request()")

async def on_response(self, profile: Profile, payload: dict):
self.logger.info("> on_response()")
invitation = await is_reusable_invitation(profile, payload)
invitation = await is_multi_use_invitation(profile, payload)
if invitation:
self.logger.debug(
f"on_response >> reusable invitation ({invitation.invitation_key})"
f"on_response >> multi_use invitation ({invitation.invitation_key})"
)
# ok, this is one of our invitations...

Expand All @@ -78,5 +78,5 @@ async def on_response(self, profile: Profile, payload: dict):
await db.execute(stmt)
await db.commit()
else:
self.logger.debug("on_response >> not a reusable invitation")
self.logger.debug("on_response >> not a multi_use invitation")
self.logger.info("< on_response()")
22 changes: 11 additions & 11 deletions services/traction/api/services/v1/invitations_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
from api.services import connections
from api.db.models.v1.connection_invitation import ConnectionInvitation
from api.endpoints.models.v1.invitations import (
CreateReusableInvitationPayload,
CreateReusableInvitationResponse,
CreateMultiUseInvitationPayload,
CreateMultiUseInvitationResponse,
InvitationListParameters,
InvitationItem,
InvitationAcapy,
Expand All @@ -31,22 +31,22 @@
logger = logging.getLogger(__name__)


async def create_reusable_invitation(
async def create_multi_use_invitation(
db: AsyncSession,
tenant_id: UUID,
wallet_id: UUID,
payload: CreateReusableInvitationPayload,
payload: CreateMultiUseInvitationPayload,
) -> [InvitationItem, str]:
"""Create ReusableInvitation.
"""Create MultiUseInvitation.
Create a reusable Invitation, this can be called by many different agents each use
Create a multi_use Invitation, this can be called by many different agents each use
of this invitation will result in a new Contact
Args:
db: database session
tenant_id: Traction ID of tenant making the call
wallet_id: AcaPy Wallet ID for tenant
payload: Reusable Invitation data
payload: MultiUse Invitation data
Returns:
item: The Traction Invitation
Expand All @@ -59,8 +59,8 @@ async def create_reusable_invitation(
existing_connection = connections.get_connection_with_alias(payload.name)
if existing_connection is not None:
raise AlreadyExistsError(
code="invitation.create.reusable.invitation.existing.alias",
title="Create Reusable Invitation alias in use",
code="invitation.create.multi_use.invitation.existing.alias",
title="Create MultiUse Invitation alias in use",
detail=f"Error alias {payload.name} already in use.",
)

Expand All @@ -78,7 +78,7 @@ async def create_reusable_invitation(
tags=payload.tags,
tenant_id=tenant_id,
public=False,
reusable=True,
multi_use=True,
status=InvitationStatusType.active,
state=connection.state,
connection=connection,
Expand All @@ -101,7 +101,7 @@ async def list_invitations(
tenant_id: UUID,
wallet_id: UUID,
parameters: InvitationListParameters,
) -> [List[CreateReusableInvitationResponse], int]:
) -> [List[CreateMultiUseInvitationResponse], int]:
"""List Invitations.
Return a page of invitations filtered by given parameters.
Expand Down
19 changes: 19 additions & 0 deletions services/traction/bdd-tests/features/steps/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ def step_impl(context, inviter: str, invitee: str):
}


@given('"{inviter}" creates a multi-use invitation')
@when('"{inviter}" creates a multi-use invitation')
def step_impl(context, inviter: str):
response = requests.post(
context.config.userdata.get("traction_host")
+ "/tenant/v1/invitations/create-multi-use-invitation",
json={"name": "testing"},
headers=context.config.userdata[inviter]["auth_headers"],
)
assert response.status_code == status.HTTP_200_OK, response.__dict__

resp_json = json.loads(response.content)
response.status_code == status.HTTP_200_OK, resp_json
context.config.userdata[inviter]["invitation"] = {
"invitation": resp_json["item"]["acapy"]["invitation"],
"invitation_url": resp_json["invitation_url"],
}


@given('"{invitee}" receives the invitation from "{inviter}"')
@when('"{invitee}" receives the invitation from "{inviter}"')
def step_impl(context, invitee: str, inviter: str):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Feature: multi-use-invitation
Scenario: One invitation used multiple times
Given we have authenticated at the innkeeper
And we have "3" traction tenants
| name | role |
| alice | inviter |
| faber | invitee1 |
| bob | invitee2|
And "alice" creates a multi-use invitation
When "faber" receives the invitation from "alice"
And "bob" receives the invitation from "alice"
Then "faber" has a connection to "alice" in status "Active"
And "bob" has a connection to "alice" in status "Active"
And we sadly wait for 10 seconds because we have not figured out how to listen for events
Then "alice" has a connection to "faber" in status "Active"
And "alice" has a connection to "bob" in status "Active"

0 comments on commit e0fd1d5

Please sign in to comment.