anip-graphql 0.11.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: anip-graphql
3
+ Version: 0.11.0
4
+ Summary: ANIP GraphQL bindings — expose ANIPService capabilities via GraphQL
5
+ Author-email: ANIP Protocol <team@anip.dev>
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/anip-protocol/anip
8
+ Keywords: anip,agent,protocol
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: anip-service==0.11.0
16
+ Requires-Dist: fastapi>=0.115.0
17
+ Requires-Dist: ariadne>=0.24.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == "dev"
20
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
@@ -0,0 +1,15 @@
1
+ # anip-graphql
2
+
3
+ ANIP GraphQL bindings — expose ANIPService capabilities via GraphQL
4
+
5
+ Part of the [ANIP](https://github.com/anip-protocol/anip) protocol ecosystem.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install anip-graphql
11
+ ```
12
+
13
+ ## Documentation
14
+
15
+ See the [ANIP repository](https://github.com/anip-protocol/anip) for full documentation.
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "anip-graphql"
3
+ version = "0.11.0"
4
+ description = "ANIP GraphQL bindings — expose ANIPService capabilities via GraphQL"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "anip-service==0.11.0",
8
+ "fastapi>=0.115.0",
9
+ "ariadne>=0.24.0",
10
+ ]
11
+ authors = [{ name = "ANIP Protocol", email = "team@anip.dev" }]
12
+ license = { text = "Apache-2.0" }
13
+ keywords = ["anip", "agent", "protocol"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "License :: OSI Approved :: Apache Software License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.11",
19
+ "Programming Language :: Python :: 3.12",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ dev = [
24
+ "pytest>=8.0",
25
+ "httpx>=0.27.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/anip-protocol/anip"
30
+
31
+ [build-system]
32
+ requires = ["setuptools>=68.0"]
33
+ build-backend = "setuptools.build_meta"
34
+
35
+ [tool.pytest.ini_options]
36
+ asyncio_mode = "auto"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ """ANIP GraphQL bindings — expose ANIPService capabilities via GraphQL."""
2
+ from .routes import mount_anip_graphql
3
+
4
+ __all__ = ["mount_anip_graphql"]
@@ -0,0 +1,157 @@
1
+ """ANIP GraphQL bindings — mount a GraphQL endpoint on a FastAPI app."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from ariadne import QueryType, MutationType, ScalarType, make_executable_schema
7
+ from ariadne.asgi import GraphQL
8
+ from fastapi import FastAPI
9
+ from fastapi.responses import PlainTextResponse
10
+
11
+ from anip_service import ANIPService, ANIPError
12
+ from .translation import (
13
+ generate_schema,
14
+ build_graphql_response,
15
+ to_camel_case,
16
+ to_snake_case,
17
+ )
18
+
19
+
20
+ async def _resolve_auth(request, service: ANIPService, capability_name: str):
21
+ """Resolve auth — JWT first, then API key."""
22
+ auth = request.headers.get("authorization", "")
23
+ if not auth.startswith("Bearer "):
24
+ return None
25
+ bearer = auth[7:].strip()
26
+
27
+ # Try as JWT first — preserves original delegation chain
28
+ jwt_error = None
29
+ try:
30
+ return await service.resolve_bearer_token(bearer)
31
+ except ANIPError as e:
32
+ jwt_error = e
33
+
34
+ # Try as API key — only if JWT failed
35
+ principal = await service.authenticate_bearer(bearer)
36
+ if principal:
37
+ cap_decl = service.get_capability_declaration(capability_name)
38
+ min_scope = cap_decl.minimum_scope if cap_decl else []
39
+ token_result = await service.issue_token(principal, {
40
+ "subject": "adapter:anip-graphql",
41
+ "scope": min_scope if min_scope else ["*"],
42
+ "capability": capability_name,
43
+ "purpose_parameters": {"source": "graphql"},
44
+ })
45
+ return await service.resolve_bearer_token(token_result["token"])
46
+
47
+ # Surface the original JWT error
48
+ if jwt_error:
49
+ raise jwt_error
50
+ return None
51
+
52
+
53
+ def _make_resolver(capability_name: str, service: ANIPService):
54
+ """Create a resolver for a given capability."""
55
+
56
+ async def resolver(_obj: Any, info: Any, **kwargs: Any) -> dict[str, Any]:
57
+ # Convert camelCase args back to snake_case
58
+ arguments = {to_snake_case(k): v for k, v in kwargs.items()}
59
+
60
+ try:
61
+ token = await _resolve_auth(info.context["request"], service, capability_name)
62
+ except ANIPError as e:
63
+ return build_graphql_response({
64
+ "success": False,
65
+ "failure": {
66
+ "type": e.error_type,
67
+ "detail": e.detail,
68
+ "resolution": e.resolution,
69
+ "retry": e.retry,
70
+ },
71
+ })
72
+
73
+ if token is None:
74
+ return build_graphql_response({
75
+ "success": False,
76
+ "failure": {
77
+ "type": "authentication_required",
78
+ "detail": "Authorization header required",
79
+ "resolution": {"action": "provide_credentials"},
80
+ "retry": True,
81
+ },
82
+ })
83
+
84
+ try:
85
+ result = await service.invoke(capability_name, token, arguments)
86
+ except ANIPError as e:
87
+ return build_graphql_response({
88
+ "success": False,
89
+ "failure": {
90
+ "type": e.error_type,
91
+ "detail": e.detail,
92
+ "resolution": e.resolution,
93
+ "retry": e.retry,
94
+ },
95
+ })
96
+
97
+ return build_graphql_response(result)
98
+
99
+ return resolver
100
+
101
+
102
+ def mount_anip_graphql(
103
+ app: FastAPI,
104
+ service: ANIPService,
105
+ *,
106
+ path: str = "/graphql",
107
+ prefix: str = "",
108
+ debug: bool = False,
109
+ ) -> None:
110
+ """Mount a GraphQL endpoint on a FastAPI app.
111
+
112
+ Args:
113
+ app: FastAPI app instance.
114
+ service: ANIPService to expose.
115
+ path: GraphQL endpoint path. Default: "/graphql".
116
+ prefix: URL prefix.
117
+ """
118
+ manifest = service.get_manifest()
119
+ capabilities = {}
120
+ for name in manifest.capabilities:
121
+ decl = service.get_capability_declaration(name)
122
+ if decl:
123
+ capabilities[name] = decl
124
+
125
+ schema_sdl = generate_schema(capabilities)
126
+
127
+ # Build Ariadne resolvers
128
+ query = QueryType()
129
+ mutation = MutationType()
130
+ json_scalar = ScalarType("JSON")
131
+
132
+ @json_scalar.serializer
133
+ def serialize_json(value):
134
+ return value
135
+
136
+ @json_scalar.value_parser
137
+ def parse_json_value(value):
138
+ return value
139
+
140
+ for name, decl in capabilities.items():
141
+ camel_name = to_camel_case(name)
142
+ resolver_fn = _make_resolver(name, service)
143
+ se_type = decl.side_effect.type.value if hasattr(decl.side_effect.type, "value") else str(decl.side_effect.type)
144
+ if se_type == "read":
145
+ query.field(camel_name)(resolver_fn)
146
+ else:
147
+ mutation.field(camel_name)(resolver_fn)
148
+
149
+ schema = make_executable_schema(schema_sdl, query, mutation, json_scalar)
150
+ graphql_app = GraphQL(schema, debug=debug)
151
+
152
+ full_path = f"{prefix}{path}"
153
+ app.mount(full_path, graphql_app)
154
+
155
+ @app.get(f"{prefix}/schema.graphql")
156
+ async def get_schema() -> PlainTextResponse:
157
+ return PlainTextResponse(schema_sdl, media_type="text/plain")
@@ -0,0 +1,161 @@
1
+ """ANIP → GraphQL translation layer.
2
+
3
+ Generates SDL schema from ANIP capabilities with custom directives,
4
+ camelCase field names, and query/mutation separation.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from anip_core.models import CapabilityDeclaration
11
+
12
+
13
+ def to_camel_case(snake: str) -> str:
14
+ parts = snake.split("_")
15
+ return parts[0] + "".join(p.capitalize() for p in parts[1:])
16
+
17
+
18
+ def to_snake_case(camel: str) -> str:
19
+ import re
20
+ return re.sub(r"([A-Z])", r"_\1", camel).lower().lstrip("_")
21
+
22
+
23
+ def _to_pascal_case(snake: str) -> str:
24
+ return "".join(p.capitalize() for p in snake.split("_"))
25
+
26
+
27
+ _GQL_TYPE_MAP = {
28
+ "string": "String",
29
+ "integer": "Int",
30
+ "number": "Float",
31
+ "boolean": "Boolean",
32
+ "object": "JSON",
33
+ "array": "JSON",
34
+ }
35
+
36
+
37
+ def generate_schema(capabilities: dict[str, CapabilityDeclaration]) -> str:
38
+ """Generate a complete GraphQL SDL schema from ANIP capabilities."""
39
+ lines = [
40
+ 'directive @anipSideEffect(type: String!, rollbackWindow: String) on FIELD_DEFINITION',
41
+ 'directive @anipCost(certainty: String!, currency: String, rangeMin: Float, rangeMax: Float) on FIELD_DEFINITION',
42
+ 'directive @anipRequires(capabilities: [String!]!) on FIELD_DEFINITION',
43
+ 'directive @anipScope(scopes: [String!]!) on FIELD_DEFINITION',
44
+ '',
45
+ 'scalar JSON',
46
+ '',
47
+ 'type CostActual { financial: FinancialCost, varianceFromEstimate: String }',
48
+ 'type FinancialCost { amount: Float, currency: String }',
49
+ 'type ANIPFailure { type: String!, detail: String!, resolution: Resolution, retry: Boolean! }',
50
+ 'type Resolution { action: String!, requires: String, grantableBy: String }',
51
+ '',
52
+ ]
53
+
54
+ queries = []
55
+ mutations = []
56
+
57
+ for name, decl in capabilities.items():
58
+ pascal = _to_pascal_case(name)
59
+ camel = to_camel_case(name)
60
+
61
+ lines.append(f"type {pascal}Result {{ success: Boolean!, result: JSON, costActual: CostActual, failure: ANIPFailure }}")
62
+
63
+ args = _build_field_args(decl)
64
+ directives = _build_directives(decl)
65
+ field_line = f" {camel}{args}: {pascal}Result! {directives}"
66
+
67
+ se_type = decl.side_effect.type.value if hasattr(decl.side_effect.type, "value") else str(decl.side_effect.type)
68
+ if se_type == "read":
69
+ queries.append(field_line)
70
+ else:
71
+ mutations.append(field_line)
72
+
73
+ lines.append("")
74
+ if queries:
75
+ lines.append("type Query {")
76
+ lines.extend(queries)
77
+ lines.append("}")
78
+ if mutations:
79
+ lines.append("type Mutation {")
80
+ lines.extend(mutations)
81
+ lines.append("}")
82
+
83
+ return "\n".join(lines)
84
+
85
+
86
+ def _build_field_args(decl: CapabilityDeclaration) -> str:
87
+ if not decl.inputs:
88
+ return ""
89
+ args = []
90
+ for inp in decl.inputs:
91
+ gql_type = _GQL_TYPE_MAP.get(inp.type, "String")
92
+ if inp.required:
93
+ gql_type += "!"
94
+ args.append(f"{to_camel_case(inp.name)}: {gql_type}")
95
+ return "(" + ", ".join(args) + ")"
96
+
97
+
98
+ def _build_directives(decl: CapabilityDeclaration) -> str:
99
+ parts = []
100
+ se_type = decl.side_effect.type.value if hasattr(decl.side_effect.type, "value") else str(decl.side_effect.type)
101
+ rollback = decl.side_effect.rollback_window
102
+
103
+ se_dir = f'@anipSideEffect(type: "{se_type}"'
104
+ if rollback:
105
+ se_dir += f', rollbackWindow: "{rollback}"'
106
+ se_dir += ")"
107
+ parts.append(se_dir)
108
+
109
+ if decl.cost:
110
+ certainty = decl.cost.certainty.value if hasattr(decl.cost.certainty, "value") else str(decl.cost.certainty)
111
+ cost_dir = f'@anipCost(certainty: "{certainty}"'
112
+ if decl.cost.financial:
113
+ financial = decl.cost.financial
114
+ currency = financial.get("currency") if isinstance(financial, dict) else getattr(financial, "currency", None)
115
+ if currency:
116
+ cost_dir += f', currency: "{currency}"'
117
+ cost_dir += ")"
118
+ parts.append(cost_dir)
119
+
120
+ if decl.requires:
121
+ cap_names = ", ".join(f'"{r.capability}"' for r in decl.requires)
122
+ parts.append(f"@anipRequires(capabilities: [{cap_names}])")
123
+
124
+ if decl.minimum_scope:
125
+ scope_vals = ", ".join(f'"{s}"' for s in decl.minimum_scope)
126
+ parts.append(f"@anipScope(scopes: [{scope_vals}])")
127
+
128
+ return " ".join(parts)
129
+
130
+
131
+ def build_graphql_response(result: dict[str, Any]) -> dict[str, Any]:
132
+ """Map ANIP invoke response to GraphQL result shape (camelCase)."""
133
+ response: dict[str, Any] = {
134
+ "success": result.get("success", False),
135
+ "result": result.get("result"),
136
+ "costActual": None,
137
+ "failure": None,
138
+ }
139
+
140
+ cost_actual = result.get("cost_actual")
141
+ if cost_actual:
142
+ response["costActual"] = {
143
+ "financial": cost_actual.get("financial"),
144
+ "varianceFromEstimate": cost_actual.get("variance_from_estimate"),
145
+ }
146
+
147
+ failure = result.get("failure")
148
+ if failure:
149
+ resolution = failure.get("resolution")
150
+ response["failure"] = {
151
+ "type": failure.get("type", "unknown"),
152
+ "detail": failure.get("detail", ""),
153
+ "resolution": {
154
+ "action": resolution.get("action", ""),
155
+ "requires": resolution.get("requires"),
156
+ "grantableBy": resolution.get("grantable_by"),
157
+ } if resolution else None,
158
+ "retry": failure.get("retry", False),
159
+ }
160
+
161
+ return response
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: anip-graphql
3
+ Version: 0.11.0
4
+ Summary: ANIP GraphQL bindings — expose ANIPService capabilities via GraphQL
5
+ Author-email: ANIP Protocol <team@anip.dev>
6
+ License: Apache-2.0
7
+ Project-URL: Repository, https://github.com/anip-protocol/anip
8
+ Keywords: anip,agent,protocol
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: anip-service==0.11.0
16
+ Requires-Dist: fastapi>=0.115.0
17
+ Requires-Dist: ariadne>=0.24.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0; extra == "dev"
20
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/anip_graphql/__init__.py
4
+ src/anip_graphql/routes.py
5
+ src/anip_graphql/translation.py
6
+ src/anip_graphql.egg-info/PKG-INFO
7
+ src/anip_graphql.egg-info/SOURCES.txt
8
+ src/anip_graphql.egg-info/dependency_links.txt
9
+ src/anip_graphql.egg-info/requires.txt
10
+ src/anip_graphql.egg-info/top_level.txt
11
+ tests/test_graphql.py
@@ -0,0 +1,7 @@
1
+ anip-service==0.11.0
2
+ fastapi>=0.115.0
3
+ ariadne>=0.24.0
4
+
5
+ [dev]
6
+ pytest>=8.0
7
+ httpx>=0.27.0
@@ -0,0 +1 @@
1
+ anip_graphql
@@ -0,0 +1,172 @@
1
+ """Tests for the anip-graphql package."""
2
+ import pytest
3
+
4
+ from anip_core.models import (
5
+ CapabilityDeclaration, CapabilityInput, CapabilityOutput,
6
+ SideEffect, SideEffectType,
7
+ )
8
+ from anip_graphql.translation import (
9
+ generate_schema, to_camel_case, to_snake_case, build_graphql_response,
10
+ )
11
+
12
+ GREET_DECL = CapabilityDeclaration(
13
+ name="greet",
14
+ description="Say hello",
15
+ inputs=[CapabilityInput(name="name", type="string", required=True, description="Who")],
16
+ output=CapabilityOutput(type="object", fields=["message"]),
17
+ side_effect=SideEffect(type=SideEffectType.READ, rollback_window="not_applicable"),
18
+ minimum_scope=["greet"],
19
+ )
20
+
21
+ BOOK_DECL = CapabilityDeclaration(
22
+ name="book_item",
23
+ description="Book something",
24
+ inputs=[CapabilityInput(name="item_name", type="string", required=True, description="What")],
25
+ output=CapabilityOutput(type="object", fields=["booking_id"]),
26
+ side_effect=SideEffect(type=SideEffectType.IRREVERSIBLE, rollback_window="none"),
27
+ minimum_scope=["book"],
28
+ )
29
+
30
+
31
+ class TestCaseConversion:
32
+ def test_to_camel_case(self):
33
+ assert to_camel_case("search_flights") == "searchFlights"
34
+ assert to_camel_case("book") == "book"
35
+
36
+ def test_to_snake_case(self):
37
+ assert to_snake_case("searchFlights") == "search_flights"
38
+ assert to_snake_case("itemName") == "item_name"
39
+
40
+
41
+ class TestSDLGeneration:
42
+ def test_generates_query_for_read(self):
43
+ sdl = generate_schema({"greet": GREET_DECL})
44
+ assert "type Query" in sdl
45
+ assert "greet(name: String!): GreetResult!" in sdl
46
+
47
+ def test_generates_mutation_for_irreversible(self):
48
+ sdl = generate_schema({"book_item": BOOK_DECL})
49
+ assert "type Mutation" in sdl
50
+ assert "bookItem(itemName: String!): BookItemResult!" in sdl
51
+
52
+ def test_includes_directives(self):
53
+ sdl = generate_schema({"greet": GREET_DECL})
54
+ assert "@anipSideEffect" in sdl
55
+ assert "@anipScope" in sdl
56
+
57
+ def test_includes_shared_types(self):
58
+ sdl = generate_schema({"greet": GREET_DECL})
59
+ assert "type ANIPFailure" in sdl
60
+ assert "scalar JSON" in sdl
61
+
62
+
63
+ class TestResponseMapping:
64
+ def test_success_response(self):
65
+ result = build_graphql_response({"success": True, "result": {"message": "Hi"}})
66
+ assert result["success"] is True
67
+ assert result["result"]["message"] == "Hi"
68
+
69
+ def test_failure_response(self):
70
+ result = build_graphql_response({
71
+ "success": False,
72
+ "failure": {
73
+ "type": "scope_insufficient",
74
+ "detail": "Missing scope",
75
+ "resolution": {"action": "request_scope", "grantable_by": "admin"},
76
+ "retry": False,
77
+ },
78
+ })
79
+ assert result["success"] is False
80
+ assert result["failure"]["type"] == "scope_insufficient"
81
+ assert result["failure"]["resolution"]["grantableBy"] == "admin"
82
+
83
+ def test_cost_actual_mapping(self):
84
+ result = build_graphql_response({
85
+ "success": True,
86
+ "result": {},
87
+ "cost_actual": {
88
+ "financial": {"amount": 100, "currency": "USD"},
89
+ "variance_from_estimate": "-5%",
90
+ },
91
+ })
92
+ assert result["costActual"]["financial"]["amount"] == 100
93
+ assert result["costActual"]["varianceFromEstimate"] == "-5%"
94
+
95
+
96
+ class TestMountIntegration:
97
+ """Integration tests using a real ANIPService + FastAPI TestClient."""
98
+
99
+ API_KEY = "test-key"
100
+
101
+ @pytest.fixture
102
+ def client(self):
103
+ from fastapi import FastAPI
104
+ from fastapi.testclient import TestClient
105
+ from anip_service import ANIPService, Capability
106
+ from anip_fastapi import mount_anip
107
+ from anip_graphql import mount_anip_graphql
108
+
109
+ service = ANIPService(
110
+ service_id="test-graphql",
111
+ capabilities=[
112
+ Capability(
113
+ declaration=GREET_DECL,
114
+ handler=lambda ctx, params: {"message": f"Hello, {params['name']}!"},
115
+ ),
116
+ Capability(
117
+ declaration=BOOK_DECL,
118
+ handler=lambda ctx, params: {"booking_id": "BK-001"},
119
+ ),
120
+ ],
121
+ authenticate=lambda bearer: "test-agent" if bearer == self.API_KEY else None,
122
+ )
123
+ app = FastAPI()
124
+ mount_anip(app, service) # owns lifecycle via app hooks
125
+ mount_anip_graphql(app, service) # adds GraphQL routes only
126
+ return TestClient(app)
127
+
128
+ def test_query_read_capability(self, client):
129
+ resp = client.post(
130
+ "/graphql",
131
+ json={"query": '{ greet(name: "World") { success result } }'},
132
+ headers={"Authorization": f"Bearer {self.API_KEY}"},
133
+ )
134
+ assert resp.status_code == 200
135
+ data = resp.json()
136
+ assert data["data"]["greet"]["success"] is True
137
+ assert data["data"]["greet"]["result"]["message"] == "Hello, World!"
138
+
139
+ def test_mutation_write_capability(self, client):
140
+ resp = client.post(
141
+ "/graphql",
142
+ json={"query": 'mutation { bookItem(itemName: "x") { success result } }'},
143
+ headers={"Authorization": f"Bearer {self.API_KEY}"},
144
+ )
145
+ assert resp.status_code == 200
146
+ assert resp.json()["data"]["bookItem"]["success"] is True
147
+
148
+ def test_query_with_invalid_jwt_returns_invalid_token(self, client):
149
+ resp = client.post(
150
+ "/graphql",
151
+ json={"query": '{ greet(name: "World") { success failure { type } } }'},
152
+ headers={"Authorization": "Bearer garbage-not-a-jwt"},
153
+ )
154
+ assert resp.status_code == 200
155
+ data = resp.json()
156
+ assert data["data"]["greet"]["success"] is False
157
+ assert data["data"]["greet"]["failure"]["type"] == "invalid_token"
158
+
159
+ def test_query_without_auth_returns_failure(self, client):
160
+ resp = client.post(
161
+ "/graphql",
162
+ json={"query": '{ greet(name: "World") { success failure { type } } }'},
163
+ )
164
+ assert resp.status_code == 200
165
+ data = resp.json()
166
+ assert data["data"]["greet"]["success"] is False
167
+ assert data["data"]["greet"]["failure"]["type"] == "authentication_required"
168
+
169
+ def test_schema_endpoint(self, client):
170
+ resp = client.get("/schema.graphql")
171
+ assert resp.status_code == 200
172
+ assert "type Query" in resp.text