Repository: tartiflette/tartiflette (python)

Issue:
OBJECT supporting directive does not run if object is a part of union
* [ ] **Explain with a simple sentence the expected behavior**
I think the title covers the problem pretty much. The problem is easily reproducible.
I'm sorry, I can't provide the `Dockerfile` example. The image we're using is `python:3.8-alpine`, if that carries any weight.

Interestingly, placing the directive on the union definition will run the directive, despite the directive not supporting type UNION.

* [ ] **Tartiflette version:** _1.1.3_
* [ ] **Python version:** _3.8_
* [ ] **Executed in docker:** Yes
* [ ] **GraphQL Schema & Query:**
```
schema {
  query: Query
}

directive @run_me_this on OBJECT

type Query {
  rootQueryField: CoupleOfResponses
} 

union CoupleOfResponses = ResponseOne | ResponseTwo

type ResponseOne @run_me_this {
...
}


type ResponseTwo {
...
}
```

```
# code
class RunMeThis("run_me_this"):
  async def on_pre_output_coercion(self, directive_args, next_directive, value, ctx, info):
    # never runs
    return await next_directive(value, ctx, info)
```

```
# the request
query {
  rootQueryField {
    ...on ResponseOne {
      ...
    }
  }
}

Pull request:
ISSUE-381, find a way to call post_out on type when they are accessed via a Union.
closes #381 

The idea is to calls the coercion of the runtimeType when union complete object is done. 
It does some reg, on 4 tests that I will investigate, but maybe @Maximilien-R you can already give me feedbacks on it, if you think its not to stupid.

Coercing the Union -> complete the object -> coerce_the_object (maybe it will call complete object to much :/) -> call the directive on the object. I don't know if I'm really clear on this @Maximilien-R ?

I'll continue to dive In.



Patch:
diff --git a/tartiflette/coercers/outputs/abstract_coercer.py b/tartiflette/coercers/outputs/abstract_coercer.py
index 52eca9d..13d23b9 100644
--- a/tartiflette/coercers/outputs/abstract_coercer.py
+++ b/tartiflette/coercers/outputs/abstract_coercer.py
@@ -97,20 +97,25 @@ async def abstract_coercer(
         execution_context.schema.default_type_resolver,
     )
 
-    return await complete_object_value(
+    runtime_type = ensure_valid_runtime_type(
+        type_resolver(result, execution_context.context, info, abstract_type),
+        execution_context,
+        abstract_type,
+        field_nodes,
+        info,
         result,
+    )
+
+    return await complete_object_value(
+        await runtime_type.pre_output_coercion_directives(
+            result,
+            execution_context.context,
+            info,
+            context_coercer=execution_context.context,
+        ),
         info,
         execution_context,
         field_nodes,
         path,
-        ensure_valid_runtime_type(
-            type_resolver(
-                result, execution_context.context, info, abstract_type
-            ),
-            execution_context,
-            abstract_type,
-            field_nodes,
-            info,
-            result,
-        ),
+        runtime_type,
     )
diff --git a/tartiflette/types/object.py b/tartiflette/types/object.py
index ee73528..53d3a0f 100644
--- a/tartiflette/types/object.py
+++ b/tartiflette/types/object.py
@@ -54,6 +54,7 @@ class GraphQLObjectType(GraphQLCompositeType, GraphQLType):
         # Directives
         self.directives = directives
         self.introspection_directives: Optional[Callable] = None
+        self.pre_output_coercion_directives: Optional[Callable] = None
 
         # Coercers
         self.output_coercer: Optional[Callable] = None
@@ -146,15 +147,17 @@ class GraphQLObjectType(GraphQLCompositeType, GraphQLType):
             directive_hook="on_introspection",
         )
 
+        self.pre_output_coercion_directives = wraps_with_directives(
+            directives_definition=directives_definition,
+            directive_hook="on_pre_output_coercion",
+            with_default=True,
+        )
+
         # Coercers
         self.output_coercer = partial(
             output_directives_coercer,
             coercer=partial(object_coercer, object_type=self),
-            directives=wraps_with_directives(
-                directives_definition=directives_definition,
-                directive_hook="on_pre_output_coercion",
-                with_default=True,
-            ),
+            directives=self.pre_output_coercion_directives,
         )
 
     async def bake_fields(


Test patch:
diff --git a/tests/functional/regressions/test_issue381.py b/tests/functional/regressions/test_issue381.py
new file mode 100644
index 0000000..26357ff
--- /dev/null
+++ b/tests/functional/regressions/test_issue381.py
@@ -0,0 +1,179 @@
+import pytest
+
+from tartiflette import Directive, Resolver, TypeResolver, create_engine
+
+
+@pytest.fixture(scope="module")
+async def ttftt_engine_union():
+    schema_name = "test_issue381"
+
+    sdl = """ schema {
+        query: Query
+    }
+
+    directive @run_me_this_object on OBJECT
+    directive @run_me_this_ifa on INTERFACE
+    directive @run_me_this_union on UNION
+
+    type Query {
+        aFieldUnionObj: CoupleOfResponses
+        aFieldObj: CoupleOfResponsesNoUnionDir
+        aFieldIfa: AnIfa
+        aFieldAnotherIfa: AnotherIfa
+        aFieldIfaNoDir: NoDirOnThisIface
+    }
+
+    union CoupleOfResponses @run_me_this_union = ResponseOne | ResponseTwo
+    union CoupleOfResponsesNoUnionDir = ResponseOne | ResponseTwo
+
+    interface AnIfa @run_me_this_ifa {
+        a: String
+    }
+
+    interface AnotherIfa @run_me_this_ifa {
+        a: String
+    }
+
+    interface NoDirOnThisIface {
+        a: String
+    }
+
+    type ResponseOne @run_me_this_object {
+        a: String
+    }
+
+    type ResponseThree implements AnIfa {
+        a: String
+    }
+
+    type ResponseFour implements AnotherIfa @run_me_this_object {
+        a: String
+    }
+
+    type ResponseFive implements NoDirOnThisIface @run_me_this_object {
+        a: String
+    }
+
+    type ResponseTwo {
+        b: Int
+    }
+    """
+
+    @Directive(name="run_me_this_object", schema_name=schema_name)
+    class RunMeThisObject:
+        async def on_pre_output_coercion(
+            self, directive_args, next_directive, value, ctx, info
+        ):
+            bob = await next_directive(value, ctx, info)
+            return {"a": f"{bob['a']} obj"}
+
+    @Directive(name="run_me_this_union", schema_name=schema_name)
+    class RunMeThisUnion:
+        async def on_pre_output_coercion(
+            self, directive_args, next_directive, value, ctx, info
+        ):
+            bob = await next_directive(value, ctx, info)
+            return {"a": f"{bob['a']} union"}
+
+    @Directive(name="run_me_this_ifa", schema_name=schema_name)
+    class RunMeThisIfa:
+        async def on_pre_output_coercion(
+            self, directive_args, next_directive, value, ctx, info
+        ):
+            bob = await next_directive(value, ctx, info)
+            return {"a": f"{bob['a']} ifa"}
+
+    @Resolver("Query.aFieldUnionObj", schema_name=schema_name)
+    @Resolver("Query.aFieldObj", schema_name=schema_name)
+    @Resolver("Query.aFieldIfa", schema_name=schema_name)
+    @Resolver("Query.aFieldAnotherIfa", schema_name=schema_name)
+    @Resolver("Query.aFieldIfaNoDir", schema_name=schema_name)
+    async def resolve_root_query_field(pr, args, ctx, info):
+        return {"a": "LOOOL"}
+
+    @TypeResolver("CoupleOfResponses", schema_name=schema_name)
+    @TypeResolver("CoupleOfResponsesNoUnionDir", schema_name=schema_name)
+    def resolve_type_couple_of_responses(result, context, info, abstract_type):
+        if "a" in result:
+            return "ResponseOne"
+        return "ResponseTwo"
+
+    @TypeResolver("AnIfa", schema_name=schema_name)
+    def resolve_type_ifa(result, context, info, abstract_type):
+        return "ResponseThree"
+
+    @TypeResolver("AnotherIfa", schema_name=schema_name)
+    def resolve_type_ifa(result, context, info, abstract_type):
+        return "ResponseFour"
+
+    @TypeResolver("NoDirOnThisIface", schema_name=schema_name)
+    def resolve_type_ifa(result, context, info, abstract_type):
+        return "ResponseFive"
+
+    return await create_engine(sdl=sdl, schema_name=schema_name)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize(
+    "query,expected",
+    [
+        (
+            """
+query {
+    aFieldUnionObj {
+        ...on ResponseOne {
+            a
+        }
+    }
+}
+""",
+            {"data": {"aFieldUnionObj": {"a": "LOOOL union obj"}}},
+        ),
+        (
+            """
+query {
+    aFieldObj {
+        ...on ResponseOne {
+            a
+        }
+    }
+}
+            """,
+            {"data": {"aFieldObj": {"a": "LOOOL obj"}}},
+        ),
+        (
+            """
+query {
+    aFieldIfa {
+        ...on ResponseThree { a }
+    }
+}
+            """,
+            {"data": {"aFieldIfa": {"a": "LOOOL ifa"}}},
+        ),
+        (
+            """
+query {
+    aFieldAnotherIfa {
+        ...on ResponseFour { a }
+    }
+}
+            """,
+            {"data": {"aFieldAnotherIfa": {"a": "LOOOL ifa obj"}}},
+        ),
+        (
+            """
+query {
+    aFieldIfaNoDir {
+        ...on ResponseFive { a }
+    }
+}
+            """,
+            {"data": {"aFieldIfaNoDir": {"a": "LOOOL obj"}}},
+        ),
+    ],
+)
+async def test_issue381_output_coercion_on_union(
+    ttftt_engine_union, query, expected
+):
+    assert await ttftt_engine_union.execute(query) == expected