Skip to content

Commit

Permalink
Make qrCode query and QrCode.box return unions
Browse files Browse the repository at this point in the history
The unions may contain error types with detailed info
  • Loading branch information
pylipp committed Sep 9, 2024
1 parent e7160d0 commit 564cafe
Show file tree
Hide file tree
Showing 14 changed files with 107 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from ariadne import ObjectType

from ....authz import authorize_for_reading_box
from ....authz import authorize_for_reading_box, handle_unauthorized
from ....models.definitions.box import Box
from ....models.definitions.location import Location

qr_code = ObjectType("QrCode")


@qr_code.field("box")
@handle_unauthorized
def resolve_qr_code_box(qr_code_obj, _):
try:
box = (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from ariadne import QueryType

from ....authz import authorize
from ....authz import authorize, handle_unauthorized
from ....models.definitions.qr_code import QrCode
from ....models.utils import handle_non_existing_resource

query = QueryType()

Expand All @@ -17,6 +18,8 @@ def resolve_qr_exists(*_, qr_code):


@query.field("qrCode")
def resolve_qr_code(*_, qr_code):
@handle_unauthorized
@handle_non_existing_resource
def resolve_qr_code(*_, code):
authorize(permission="qr:read")
return QrCode.get(QrCode.code == qr_code)
return QrCode.get(QrCode.code == code)
2 changes: 2 additions & 0 deletions back/boxtribute_server/graph_ql/bindables.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ def resolve_location_type(obj, *_):
UnionType("MoveBoxesResult", resolve_type_by_class_name),
UnionType("AssignTagToBoxesResult", resolve_type_by_class_name),
UnionType("UnassignTagFromBoxesResult", resolve_type_by_class_name),
UnionType("QrCodeResult", resolve_type_by_class_name),
UnionType("BoxResult", resolve_type_by_class_name),
)
interface_types = (
InterfaceType("Location", resolve_location_type),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ type Query {
box(labelIdentifier: String!): Box
" Return page of non-deleted [`Boxes`]({{Types.Box}}) in base with specified ID. Optionally pass filters "
boxes(baseId: ID!, paginationInput: PaginationInput, filterInput: FilterBoxInput): BoxPage!
" Return [`QrCode`]({{Types.QrCode}}) with specified code (an MD5 hash in hex format of length 32) "
qrCode(qrCode: String!): QrCode
" Return [`QrCode`]({{Types.QrCode}}) with specified code (an MD5 hash in hex format of length 32), or an error in case of insufficient permission or missing resource. "
qrCode(code: String!): QrCodeResult!
qrExists(qrCode: String): Boolean
" Return [`ClassicLocation`]({{Types.ClassicLocation}}) with specified ID. Accessible for clients who are members of the location's base "
location(id: ID!): ClassicLocation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ Representation of a QR code, possibly associated with a [`Box`]({{Types.Box}}).
type QrCode {
id: ID!
code: String!
" [`Box`]({{Types.Box}}) associated with the QR code (`null` if none associated) "
box: Box
" [`Box`]({{Types.Box}}) associated with the QR code (`null` if none associated), or an error in case of insufficient permission or missing authorization for box's base "
box: BoxResult
createdOn: Datetime
}

Expand Down Expand Up @@ -617,8 +617,11 @@ union DeleteProductResult = Product | InsufficientPermissionError | ResourceDoes
union EnableStandardProductResult = Product | InsufficientPermissionError | ResourceDoesNotExistError | UnauthorizedForBaseError | InvalidPriceError | StandardProductAlreadyEnabledForBaseError
union EditStandardProductInstantiationResult = Product | InsufficientPermissionError | ResourceDoesNotExistError | UnauthorizedForBaseError | InvalidPriceError | ProductTypeMismatchError
union DisableStandardProductResult = Product | InsufficientPermissionError | ResourceDoesNotExistError | UnauthorizedForBaseError | BoxesStillAssignedToProductError | ProductTypeMismatchError

union StandardProductResult = StandardProduct | InsufficientPermissionError | ResourceDoesNotExistError
union StandardProductsResult = StandardProductPage | InsufficientPermissionError | UnauthorizedForBaseError
union QrCodeResult = QrCode | InsufficientPermissionError | ResourceDoesNotExistError
union BoxResult = Box | InsufficientPermissionError | UnauthorizedForBaseError

type Metrics {
"""
Expand Down
3 changes: 2 additions & 1 deletion back/test/auth0_integration_tests/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ def _assert_successful_request(*args, **kwargs):
return assert_successful_request(*args, **kwargs, endpoint=endpoint)

query = """query BoxIdAndItems {
qrCode(qrCode: "093f65e080a295f8076b1c5722a46aa2") { box { id } }
qrCode(code: "093f65e080a295f8076b1c5722a46aa2") {
...on QrCode { box { id } } }
}"""
queried_box = _assert_successful_request(auth0_client, query)["box"]
assert queried_box == {"id": "100000000"}
Expand Down
14 changes: 7 additions & 7 deletions back/test/endpoint_tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,6 @@ def test_query_non_existent_box(read_only_client):
assert "SQL" not in response.json["errors"][0]["message"]


def test_query_non_existent_qr_code(read_only_client):
# Test case 8.1.31
query = """query { qrCode(qrCode: "-1") { id } }"""
response = assert_bad_user_input(read_only_client, query)
assert "SQL" not in response.json["errors"][0]["message"]


@pytest.mark.parametrize("resource", ["base", "organisation", "user"])
def test_query_non_existent_resource_for_god_user(read_only_client, mocker, resource):
# Test case 99.1.3, 10.1.3
Expand Down Expand Up @@ -352,6 +345,13 @@ def test_mutate_resource_does_not_exist(
@pytest.mark.parametrize(
"operation,query_input,field,response",
[
# Test case 8.1.31
[
"qrCode",
'code: "0"',
"...on ResourceDoesNotExistError { id name }",
{"id": None, "name": "QrCode"},
],
# Test case 8.1.42
[
"standardProduct",
Expand Down
10 changes: 4 additions & 6 deletions back/test/endpoint_tests/test_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,8 @@ def test_box_query_by_label_identifier(
def test_box_query_by_qr_code(read_only_client, default_box, default_qr_code):
# Test case 8.1.5
query = f"""query {{
qrCode(qrCode: "{default_qr_code['code']}") {{
box {{
labelIdentifier
}}
}}
qrCode(code: "{default_qr_code['code']}") {{
...on QrCode {{ box {{ ...on Box {{ labelIdentifier }} }} }} }}
}}"""
queried_box = assert_successful_request(read_only_client, query)["box"]
assert queried_box["labelIdentifier"] == default_box["label_identifier"]
Expand Down Expand Up @@ -984,7 +981,8 @@ def _create_query(label_identifier):
return f"""query {{ box(labelIdentifier: "{label_identifier}") {{ id }} }}"""

def _create_qr_query(qr_code):
return f"""query {{ qrCode(qrCode: "{qr_code['code']}") {{ box {{ id }} }} }}"""
return f"""query {{ qrCode(code: "{qr_code['code']}") {{
...on QrCode {{ box {{ ...on Box {{ id }} }} }} }} }}"""

queries = {
str(in_transit_box["id"]): _create_query(in_transit_box["label_identifier"]),
Expand Down
10 changes: 6 additions & 4 deletions back/test/endpoint_tests/test_cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
NR_OF_CREATED_TAGS_PER_BASE,
NR_OF_DELETED_TAGS_PER_BASE,
)
from utils import assert_bad_user_input, assert_successful_request
from utils import assert_successful_request

reseed_db_path = f"{CRON_PATH}/reseed-db"
headers = [("X-AppEngine-Cron", "true")]
Expand Down Expand Up @@ -45,14 +45,16 @@ def test_reseed_db(cron_client, monkeypatch, mocker):

# Success; perform actual sourcing of seed (takes about 2s)
# Create QR code and verify that it is removed after reseeding
mutation = "mutation { createQrCode { code } }"
mutation = "mutation { createQrCode { id code } }"
response = assert_successful_request(cron_client, mutation)
code = response["code"]
response = cron_client.get(reseed_db_path, headers=headers)
assert response.status_code == 200
assert response.json == {"message": "reseed-db job executed"}
query = f"""query {{ qrCode(qrCode: "{code}") {{ id }} }}"""
response = assert_bad_user_input(cron_client, query)
query = f"""query {{ qrCode(code: "{code}") {{
...on ResourceDoesNotExistError {{ id name }} }} }}"""
response = assert_successful_request(cron_client, query)
assert response == {"id": None, "name": "QrCode"}

# Verify generation of fake data
query = "query { tags { id } }"
Expand Down
29 changes: 22 additions & 7 deletions back/test/endpoint_tests/test_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ def operation_name(operation):
[
# Test case 8.1.3
"""box( labelIdentifier: "12345678") { id }""",
# Test case 8.1.32
"""qrCode( qrCode: "1337beef" ) { id }""",
# Test case 8.1.35
"""qrExists( qrCode: "1337beef" )""",
],
Expand Down Expand Up @@ -306,8 +304,12 @@ def test_invalid_permission_for_qr_code_box(
# Verify missing stock:read permission
mock_user_for_request(mocker, permissions=["qr:read"])
code = default_qr_code["code"]
query = f"""query {{ qrCode(qrCode: "{code}") {{ box {{ id }} }} }}"""
assert_forbidden_request(read_only_client, query, value={"box": None})
query = f"""query {{ qrCode(code: "{code}") {{
...on QrCode {{
box {{ ...on InsufficientPermissionError {{ name }} }}
}} }} }}"""
response = assert_successful_request(read_only_client, query)
assert response == {"box": {"name": "stock:read"}}

# Test case 8.1.11
# Verify missing base-specific stock:read permission (the QR code belongs to a box
Expand All @@ -325,9 +327,15 @@ def test_invalid_permission_for_qr_code_box(
base_ids=[1],
)
code = another_qr_code_with_box["code"] # the associated box is in base ID 3
query = f"""query {{ qrCode(qrCode: "{code}") {{
box {{ tags {{ taggedResources {{ ...on Beneficiary {{ id }} }} }} }} }} }}"""
assert_forbidden_request(read_only_client, query, value={"box": None})
query = f"""query {{ qrCode(code: "{code}") {{
...on QrCode {{
box {{
...on UnauthorizedForBaseError {{ id name }}
...on Box {{
tags {{ taggedResources {{ ...on Beneficiary {{ id }} }} }}
}} }} }} }} }}"""
response = assert_successful_request(read_only_client, query)
assert response == {"box": {"id": "3", "name": ""}}


def test_invalid_permission_for_organisation_bases(
Expand Down Expand Up @@ -648,6 +656,13 @@ def test_mutate_unauthorized_for_base(
@pytest.mark.parametrize(
"operation,query_input,field,response",
[
# Test case 8.1.32
[
"qrCode",
'code: "1337beef"',
"...on InsufficientPermissionError { name }",
{"name": "qr:read"},
],
# Test case 8.1.43
[
"standardProduct",
Expand Down
13 changes: 7 additions & 6 deletions back/test/endpoint_tests/test_qr.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ def test_qr_code_query(read_only_client, default_box, default_qr_code):
# Test case 8.1.30
code = default_qr_code["code"]
query = f"""query {{
qrCode(qrCode: "{code}") {{
qrCode(code: "{code}") {{ ...on QrCode {{
id
code
box {{ id }}
box {{ ...on Box {{ id }} }}
createdOn
}}
}} }}
}}"""
queried_code = assert_successful_request(read_only_client, query)
assert queried_code == {
Expand All @@ -44,7 +44,8 @@ def test_qr_code_query(read_only_client, default_box, default_qr_code):
def test_code_not_associated_with_box(read_only_client, qr_code_without_box):
# Test case 8.1.2a
code = qr_code_without_box["code"]
query = f"""query {{ qrCode(qrCode: "{code}") {{ box {{ id }} }} }}"""
query = f"""query {{ qrCode(code: "{code}") {{
...on QrCode {{ box {{ ...on Box {{ id }} }} }} }} }}"""
qr_code = assert_successful_request(read_only_client, query)
assert qr_code == {"box": None}

Expand All @@ -61,10 +62,10 @@ def test_qr_code_mutation(client, box_without_qr_code):
createQrCode(boxLabelIdentifier: "{box_without_qr_code['label_identifier']}")
{{
id
box {{
box {{ ...on Box {{
id
numberOfItems
}}
}} }}
}}
}}"""
created_qr_code = assert_successful_request(client, mutation)
Expand Down
26 changes: 22 additions & 4 deletions front/src/queries/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,28 @@ export const BOX_DETAILS_BY_LABEL_IDENTIFIER_QUERY = gql`
export const GET_BOX_LABEL_IDENTIFIER_BY_QR_CODE = gql`
${BOX_BASIC_FIELDS_FRAGMENT}
query GetBoxLabelIdentifierForQrCode($qrCode: String!) {
qrCode(qrCode: $qrCode) {
code
box {
...BoxBasicFields
qrCode(code: $qrCode) {
__typename
... on QrCode {
code
box {
__typename
...on Box {
...BoxBasicFields
}
...on InsufficientPermissionError {
name
}
...on UnauthorizedForBaseError {
name
}
}
}
... on InsufficientPermissionError {
name
}
... on ResourceDoesNotExistError {
name
}
}
}
Expand Down
30 changes: 17 additions & 13 deletions front/src/types/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,7 @@ export type BoxPage = {
totalCount: Scalars['Int'];
};

/** Utility response type for box bulk-update mutations, containing both updated boxes and invalid boxes (ignored due to e.g. being deleted, in prohibited base, and/or non-existing). */
export type BoxesResult = {
__typename?: 'BoxesResult';
invalidBoxLabelIdentifiers: Array<Scalars['String']>;
updatedBoxes: Array<Box>;
};
export type BoxResult = Box | InsufficientPermissionError | UnauthorizedForBaseError;

/** Classificators for [`Box`]({{Types.Box}}) state. */
export enum BoxState {
Expand Down Expand Up @@ -279,6 +274,13 @@ export type BoxUpdateInput = {
tagIdsToBeAdded?: InputMaybe<Array<Scalars['Int']>>;
};

/** Utility response type for box bulk-update mutations, containing both updated boxes and invalid boxes (ignored due to e.g. being deleted, in prohibited base, and/or non-existing). */
export type BoxesResult = {
__typename?: 'BoxesResult';
invalidBoxLabelIdentifiers: Array<Scalars['String']>;
updatedBoxes: Array<Box>;
};

export type BoxesStillAssignedToProductError = {
__typename?: 'BoxesStillAssignedToProductError';
labelIdentifiers: Array<Scalars['String']>;
Expand Down Expand Up @@ -1194,13 +1196,15 @@ export type ProductTypeMismatchError = {
/** Representation of a QR code, possibly associated with a [`Box`]({{Types.Box}}). */
export type QrCode = {
__typename?: 'QrCode';
/** [`Box`]({{Types.Box}}) associated with the QR code (`null` if none associated) */
box?: Maybe<Box>;
/** [`Box`]({{Types.Box}}) associated with the QR code (`null` if none associated), or an error in case of insufficient permission or missing authorization for box's base */
box?: Maybe<BoxResult>;
code: Scalars['String'];
createdOn?: Maybe<Scalars['Datetime']>;
id: Scalars['ID'];
};

export type QrCodeResult = InsufficientPermissionError | QrCode | ResourceDoesNotExistError;

export type Query = {
__typename?: 'Query';
/** Return [`Base`]({{Types.Base}}) with specified ID. Accessible for clients who are members of this base. */
Expand Down Expand Up @@ -1241,8 +1245,8 @@ export type Query = {
productCategory?: Maybe<ProductCategory>;
/** Return all [`Products`]({{Types.Product}}) (incl. deleted) that the client is authorized to view. */
products: ProductPage;
/** Return [`QrCode`]({{Types.QrCode}}) with specified code (an MD5 hash in hex format of length 32) */
qrCode?: Maybe<QrCode>;
/** Return [`QrCode`]({{Types.QrCode}}) with specified code (an MD5 hash in hex format of length 32), or an error in case of insufficient permission or missing resource. */
qrCode: QrCodeResult;
qrExists?: Maybe<Scalars['Boolean']>;
/** Return [`Shipment`]({{Types.Shipment}}) with specified ID. Clients are authorized to view a shipment if they're member of either the source or the target base */
shipment?: Maybe<Shipment>;
Expand Down Expand Up @@ -1373,7 +1377,7 @@ export type QueryProductsArgs = {


export type QueryQrCodeArgs = {
qrCode: Scalars['String'];
code: Scalars['String'];
};


Expand Down Expand Up @@ -1945,7 +1949,7 @@ export type GetBoxLabelIdentifierForQrCodeQueryVariables = Exact<{
}>;


export type GetBoxLabelIdentifierForQrCodeQuery = { __typename?: 'Query', qrCode?: { __typename?: 'QrCode', code: string, box?: { __typename?: 'Box', labelIdentifier: string, state: BoxState, comment?: string | null, lastModifiedOn?: any | null, location?: { __typename?: 'ClassicLocation', id: string, base?: { __typename?: 'Base', id: string } | null } | { __typename?: 'DistributionSpot', id: string, base?: { __typename?: 'Base', id: string } | null } | null, shipmentDetail?: { __typename?: 'ShipmentDetail', id: string, shipment: { __typename?: 'Shipment', id: string } } | null } | null } | null };
export type GetBoxLabelIdentifierForQrCodeQuery = { __typename?: 'Query', qrCode: { __typename: 'InsufficientPermissionError', name: string } | { __typename: 'QrCode', code: string, box?: { __typename: 'Box', labelIdentifier: string, state: BoxState, comment?: string | null, lastModifiedOn?: any | null, location?: { __typename?: 'ClassicLocation', id: string, base?: { __typename?: 'Base', id: string } | null } | { __typename?: 'DistributionSpot', id: string, base?: { __typename?: 'Base', id: string } | null } | null, shipmentDetail?: { __typename?: 'ShipmentDetail', id: string, shipment: { __typename?: 'Shipment', id: string } } | null } | { __typename: 'InsufficientPermissionError', name: string } | { __typename: 'UnauthorizedForBaseError', name: string } | null } | { __typename: 'ResourceDoesNotExistError', name: string } };

export type CheckIfQrExistsInDbQueryVariables = Exact<{
qrCode: Scalars['String'];
Expand Down Expand Up @@ -2030,7 +2034,7 @@ export type CreateBoxMutationVariables = Exact<{
}>;


export type CreateBoxMutation = { __typename?: 'Mutation', createBox?: { __typename?: 'Box', labelIdentifier: string, qrCode?: { __typename?: 'QrCode', code: string, box?: { __typename?: 'Box', labelIdentifier: string } | null } | null } | null };
export type CreateBoxMutation = { __typename?: 'Mutation', createBox?: { __typename?: 'Box', labelIdentifier: string, qrCode?: { __typename?: 'QrCode', code: string, box?: { __typename?: 'Box', labelIdentifier: string } | { __typename?: 'InsufficientPermissionError' } | { __typename?: 'UnauthorizedForBaseError' } | null } | null } | null };

export type BoxByLabelIdentifierAndAllProductsWithBaseIdQueryVariables = Exact<{
baseId: Scalars['ID'];
Expand Down
4 changes: 3 additions & 1 deletion front/src/views/BoxCreate/BoxCreateView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ export const CREATE_BOX_MUTATION = gql`
qrCode {
code
box {
labelIdentifier
...on Box {
labelIdentifier
}
}
}
}
Expand Down

0 comments on commit 564cafe

Please sign in to comment.