How can I Unit Test GraphQL file Upload with API Platform? - symfony

In addition to my other tests against my GraphQL API Platform backend, I am attempting to test file uploads. I'm not quite sure whether the ApiPlatform\Core\Bridge\Symfony\Bundle\Test\Client class has the ability to facilitate this test, or if Symfony\Component\HttpFoundation\File\UploadedFile should be used to provide the test file as it is for a REST operation.
Here is roughly where I am at in terms of putting together this test. (With some irrelevant parts removed for simplification)
public function testCreateMediaObject(): void {
$file = new UploadedFile('fixtures/files/logo.png', 'logo.png');
$client = self::createClient();
$gql = <<<GQL
mutation UploadMediaObject(\$file: Upload!) {
uploadMediaObject(input: {file: \$file}) {
mediaObject {
id
contentUrl
}
}
}
GQL;
$response = $client->request('POST', '/api/graphql', [
'headers' => ['Content-Type' => 'application/json'],
'json' => [
'query' => $gql,
'variables' => ["file" => null],
'map' => ['0' => ['variables.file']],
'0' => $file,
]
]);
dd($response);
}
The response I get seems to indicate that the file isn't being included as expected. Something like...
Variable "$file" of non-null type "Upload!" must not be null.
Or.. if I try to attach the file by simply assigning it directly in the variables property...
$response = $client->request('POST', '/api/graphql', [
'headers' => ['Content-Type' => 'application/json'],
'json' => [
'query' => $gql,
'variables' => ["file" => $file],
]
]);
then...
Variable "$file" got invalid value []; Expected type Upload; Could not get uploaded file, be sure to conform to GraphQL multipart request specification. Instead got: []
In my most recent attempt, I changed things up quite a bit after sifting through the graphql code...
$formFields = [
'operations' => '{ "query": "mutation ($file: Upload!) { uploadMediaObject(input: {file: $file}) { mediaObject { id contentUrl } } }", "variables": { "file": null } }',
'map' => "{'0': ['variables.file']}",
'0' => DataPart::fromPath(__DIR__.'/../../fixtures/files/logo.png'),
];
$formData = new FormDataPart($formFields);
$response = $client->request('POST', '/api/graphql', [
'headers' => $formData->getPreparedHeaders()->toArray(),
'body' => $formData->bodyToString(),
]);
The problem with this last attempt is that the server isn't seeing any body parameters. So I receiving the exception
'GraphQL multipart request does not respect the specification.'
Which is found in /api-platform/core/src/GraphQl/Action/EntrypointAction.php within the parseMultipartRequest method.

After a few hours of debugging I found this solution:
$formData = new FormDataPart();
$file = new UploadedFile('src/DataFixtures/files/test.txt', 'test.txt');
$response = $this->$client->request('POST', 'api/graphql', [
'headers' => $formData->getPreparedHeaders()->toArray(),
'extra' => [
'parameters' => [
'operations' => '{ "query": "mutation UploadMediaObject($file: Upload!) { uploadMediaObject(input: {file: $file}) { mediaObject { id contentUrl } } }", "variables": { "file": null } }',
'map' => '{ "0": ["variables.file"] }',
'0' => #$file,
],
'files' => [
$file,
],
],
]);
Refrence:
https://github.com/jaydenseric/graphql-multipart-request-spec
https://api-platform.com/docs/core/graphql/

Related

Symfony 6 - How can I change file upload to multiple file uploads

I'm working on a project where a user is able to upload a file. My code works when a single file is uploaded, but I need to change it so a user is able to upload multiple files.
I want to store the files in my database as String. Currently it is stored as example: "file1.png". When uploading multiple files I would like it to be stored as "file1.png;file2.png;file3.png".
However when I add the "multiple => true" in the form, I get an error when pressing submit by the validator that the input needs to be a String.
My best guess is that I need to use Data transformers, but after reading the docs I still don't know how to approach this. ?
Data Transform
This is the controller (currently it expects a single file, as for multiple I would use foreach):
\#\[Route('/new', name: 'app_blog_new', methods: \['GET', 'POST'\])\]
\#\[IsGranted('IS_AUTHENTICATED')\]
public function new(Request $request, BlogRepository $blogRepository, SluggerInterface $slugger, MailerInterface $mailer): Response
{
$blog = new Blog();
$form = $this-\>createForm(BlogType::class, $blog);
$form-\>handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$additionalImages = $form->get('additional_images')->getData();
if ($additionalImages) {
$originalFilename = pathinfo($additionalImages->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename . '-' . uniqid() . '.' . $additionalImages->guessExtension();
try {
$additionalImages->move(
$this->getParameter('blogimage_directory'),
$newFilename
);
} catch (FileException $e) {
// ... handle exception if something happens during file upload
}
$blog->setAdditionalImages($newFilename);
}
}
If I add "multiple => true' to this form I get an "expected String" error on the front.
This is the form used to upload images to a blog:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title')
->add('additional_images', FileType::class, [
'label' => 'Additional images',
'mapped' => false,
'multiple' => true,
'required' => false,
'constraints' => [`your text`
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'image/*',
],
'mimeTypesMessage' => 'Please upload a valid image',
])
],
]);
$builder->get('additional_images')
->addModelTransformer(new CallbackTransformer(
function ($additionalAsArray) {
// transform the array to a string
return implode('; ', $additionalAsArray);
},
function ($additionalAsString) {
// transform the string back to an array
return explode('; ', $additionalAsString);
}
))
;
}
This is the blog entity class which contains the image(s)
#[ORM\Entity(repositoryClass: BlogRepository::class)]
class Blog
{
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $additional_images = null;
}
I tried adding 'multiple => true' to the form and it works, as the user is able to select multiple files. But after submitting I get "implode(): Argument #1 ($pieces) must be of type array, string given"
I found out that all I had to do was add "new All" to the form:
->add('additional_images', FileType::class, [
'label' => 'Additional images',
'mapped' => false,
'required' => false,
'multiple' => true,
'constraints' => [
new All([
new File([
'maxSize' => '1024k',
'mimeTypes' => [
'image/*',
],
'mimeTypesMessage' => 'Please upload a valid image',
])
])
],
]);
And made my controller work with an array:
$additionalImages = $form->get('additional_images')->getData();
if ($additionalImages) {
$result = array();
foreach ($additionalImages as $image)
{
$originalFilename = pathinfo($image->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename . '-' . uniqid() . '.' . $image->guessExtension();
try {
$image->move(
$this->getParameter('blogimage_directory'),
$newFilename
);
} catch (FileException $e) {
// ... handle exception if something happens during file upload
}
$result[] = $newFilename;
}
$blog->setAdditionalImages(implode(";", $result));
}

how to retrieve post response variable outside in reactphp

My code is as follows:
require __DIR__.'/vendor/autoload.php';
use Psr\Http\Message\ResponseInterface;
$loop = React\EventLoop\Factory::create();
$client = new React\Http\Browser($loop);
$data = [
'name' => [
'first' => 'Alice',
'name' => 'Smith',
],
'email' => 'alice#example.com',
'userid' => 'alice',
];
$client->post(
'https://httpbin.org/post',
[
'Content-Type' => 'application/json',
],
json_encode($data)
)->then(function (ResponseInterface $response) {
$response = (string) $response->getBody();
return $response;
});
**echo $response;**
$loop->run();
I am able to get the response inside then function(). but I cannot retrieve the response value outside.
I like to send multiple asynchronous POST requests and collect each responses and echo all of them at once.
How can I access the response variable from outside of post()->then()?
I could get the response when I need a response with deferred and promise method.
First, I assigned the request and response method to $promise.
And I was able to simply get the response with $deferred->resolve();
require __DIR__.'/vendor/autoload.php';
use Psr\Http\Message\ResponseInterface;
$loop = React\EventLoop\Factory::create();
$client = new React\Http\Browser($loop);
$deferred = new React\Promise\Deferred();
$promise = $deferred->promise();
$data = [
'name' => [
'first' => 'Alice',
'name' => 'Smith',
],
'email' => 'alice#example.com',
'userid' => 'alice',
];
$promise = $client->post(
'https://httpbin.org/post',
[
'Content-Type' => 'application/json',
],
json_encode($data)
)->then(function (ResponseInterface $response) {
$response = (string) $response->getBody();
return $response;
});
echo $deferred->resolve();
$loop->run();

How to document custom POST Action in API Platform through Swagger Decorator?

In the docs there is this example, but it shows only how to add one GET operation.
I would like to know how can I add a custom POST route to the documentation.
I am having trouble to show the example body request, with the expected values to be sent (username and email, in this example)
My attempt
<?php
// api/src/Swagger/SwaggerDecorator.php
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, $format = null, array $context = [])
{
$docs = $this->decorated->normalize($object, $format, $context);
$customDefinition = [
'tags' => [
'default'
],
'name' => 'fields',
'description' => 'Testing decorator',
'default' => 'id',
'in' => 'query',
'requestBody' =>
[
'content' => [
'application/json' => [
'schema' => [
'description' => 'abcd',
'required' => [
'username', 'email'
],
'properties' => [
'username', 'email'
],
]
]
],
'description' => 'testing'
],
];
$docs['paths']['/testing']['post']['parameters'][] = $customDefinition;
return $docs;
}
public function supportsNormalization($data, $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
}
But it doesn't work.
you should not put the whole route declaration inside the parameters array, you should create smth like this:
$docs['paths']['/testing']['post'] = $customDefinition;

How to store and log for all http request and response in wordpress?

I want to store all outgoing http request from my wordpress site. I want to store all internal and external http request and response also request made with curl. Any help should be useful to me.
Hook into WP_Http::_dispatch_request()
function fn_log_http_request_response( $wp_http_response, $request, $url ) {
$request = [
'method' => $request['method'],
'url' => $url,
'headers' => $request['headers'],
'body' => $request['body'],
];
if($wp_http_response instanceof WP_Error) {
$response = [
'errors' => $wp_http_response->errors,
'error_data' => $wp_http_response->error_data,
];
} else {
$response = [
'status' => [
'code' => wp_remote_retrieve_response_code($wp_http_response),
'message' => wp_remote_retrieve_response_message($wp_http_response),
],
'headers' => wp_remote_retrieve_headers($wp_http_response)->getAll(),
'body' => wp_remote_retrieve_body($wp_http_response),
];
}
error_log(print_r([
'request' => $request,
'response' => $response,
], true));
return $wp_http_response;
}
// hook into WP_Http::_dispatch_request()
add_filter('http_response', 'fn_log_http_request_response', 10, 3 );
Adopted from hinnerk-a
You can also try the following plugin as well
https://wordpress.org/plugins/log-http-requests

ZF3 development of RestAPI: handling Post request for

I am new to Zf3, and developing Email Restful API.
having difficulties in handling request. I can't get POST parameters.i think there is some problem in module.config.php > 'router'
Questions
1 How can I get POST variables from request.
2 I found it when I request page (using Postman)
- if I passes ID, controller call get($id) method
- if I POST variables controller calls Create($data) function.
--is this always the case ? and is it good to write zendEmail code inside create($data) method
.
bwsmail controller
(create function)
public function create($data)
{
echo $this->getRequest()->getPost('id', "no value");
// Output
// this returns no value
echo $this->getRequest();
// Output
//POST http://localhost:8080/bwsmail/mail?id=123456789 HTTP/1.1
//Cookie: PHPSESSID=p5i7o8lm2ed0iocdhkc8jvos01
//Cache-Control: no-cache
//Postman-Token: ccaf5b37-02a2-4537-bf3f-c1dc419d8ceb
//User-Agent: PostmanRuntime/7.6.1
//Accept: */*
//Host: localhost:8080
//Accept-Encoding: gzip, deflate
//Content-Length: 0
//Connection: keep-alive
$response = $this->getResponseWithHeader()->setContent( __METHOD__.' create new item of data :<b>'.'</b>');
return $response;
}
module.config.php (route)
'bwsmail' => [
'type' => Literal::class,
'options' => [
'route' => '/bwsmail',
'defaults' => [
'controller' => Controller\BwsMailController::class,
]
],
'may_terminate' => true,
'child_routes' => [
'mail' => [
'type' => 'segment',
'options' => [
'route' => '/mail[/:id]',
'constraints'=>
[
'id' => '[0-9a-zA-Z]+',
],
'defaults' => [
'controller' => Controller\BwsMailController::class,
]
],
],
],
],
if BwsMailController extends AbstractController or AbstractRestfulController you can simply: $this->params()->fromRoute() or ->fromQuery() if you're using a get string. For example:
$routeParamId = $this->params()->fromRoute('id', null);
This gets the parameter id as it is defined in the route configuration. If it's not set, it will default to null.
If you have a GET url, something like: site.com/mail?id=123, then you do:
$getParamId = $this->params()->fromQuery('id', null); // sets '123' in var
More options are available, have a good read of the routing documentation.

Resources