Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.20% covered (success)
93.20%
96 / 103
71.43% covered (warning)
71.43%
10 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
LaravelTestApplication
93.20% covered (success)
93.20%
96 / 103
71.43% covered (warning)
71.43%
10 / 14
20.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getConsoleApplication
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getApplicationConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 defineEnvironment
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
1
 bootApplication
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
2.04
 getServiceContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cleanApplication
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 httpRequestGet
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 handleResponse
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getPackageProviders
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 httpRequest
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 loginAs
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 logout
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getLoggedInAs
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
1<?php
2namespace Apie\IntegrationTests\Applications\Laravel;
3
4use Apie\AiInstructor\AiClient;
5use Apie\Common\Events\AddAuthenticationCookie;
6use Apie\Common\IntegrationTestLogger;
7use Apie\Common\ValueObjects\DecryptedAuthenticatedUser;
8use Apie\Common\Wrappers\TextEncrypter;
9use Apie\Core\Other\FileWriterInterface;
10use Apie\Core\Other\MockFileWriter;
11use Apie\IntegrationTests\Applications\MockFactory;
12use Apie\IntegrationTests\Concerns\ItRunsApplications;
13use Apie\IntegrationTests\Config\ApplicationConfig;
14use Apie\IntegrationTests\Config\BoundedContextConfig;
15use Apie\IntegrationTests\Interfaces\TestApplicationInterface;
16use Apie\IntegrationTests\Requests\TestRequestInterface;
17use Apie\LaravelApie\ApieServiceProvider;
18use Apie\LaravelApie\Config\ValidateAndSanitizeConfig;
19use Illuminate\Contracts\Config\Repository;
20use Illuminate\Contracts\Http\Kernel as HttpKernel;
21use Illuminate\Http\Request;
22use Illuminate\Support\Facades\Auth;
23use Nyholm\Psr7\Factory\Psr17Factory as NyholmPsr17Factory;
24use Orchestra\Testbench\TestCase;
25use Psr\Container\ContainerInterface;
26use Psr\Http\Message\ResponseInterface;
27use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
28use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
29use Symfony\Component\Console\Application;
30use Symfony\Component\HttpFoundation\Cookie;
31use Symfony\Component\HttpFoundation\Response as HttpFoundationResponse;
32use Symfony\Component\HttpFoundation\ResponseHeaderBag;
33use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
34
35class LaravelTestApplication extends TestCase implements TestApplicationInterface
36{
37    use ItRunsApplications;
38
39    /**
40     * @phpstan-ignore method.parentMethodFinalByPhpDoc
41     */
42    public function __construct(
43        private readonly ApplicationConfig $applicationConfig,
44        private readonly BoundedContextConfig $boundedContextConfig
45    ) {
46        parent::__construct('Laravel application');
47    }
48
49    public function getConsoleApplication(): Application
50    {
51        $application = new \Illuminate\Console\Application(
52            $this->getServiceContainer(),
53            $this->getServiceContainer()->get(\Illuminate\Contracts\Events\Dispatcher::class),
54            'test'
55        );
56        return $application;
57    }
58
59    public function getApplicationConfig(): ApplicationConfig
60    {
61        return $this->applicationConfig;
62    }
63
64    protected function defineEnvironment($app): void
65    {
66        tap($app->make('config'), function (Repository $config) {
67            $config->set('app.key', 'base64:/aNEFWQbsYwDslb4Xw1RKKj9oCdZdbNhvcyUpVgXPz4=');
68            $config->set('apie.encryption_key', 'test');
69            $config->set(
70                'apie.bounded_contexts',
71                $this->boundedContextConfig->toArray()
72            );
73            $config->set(
74                'apie.scan_bounded_contexts',
75                []
76            );
77            // this has to be explicit because of some code execution order issues
78            $config->set('apie.enable_ai_instructor', true);
79            $config->set('apie.enable_mcp_server', true);
80            $config->set('apie.enable_webdav', true);
81            $config->set(
82                'apie.ai',
83                [
84                    'base_url' => 'http://localbost:11434',
85                    'api_key' => 'test',
86                ]
87            );
88            $config->set('apie.graphql.base_url', 'graphql');
89            $config->set(
90                'apie.datalayers',
91                [
92                    'default_datalayer' => $this->applicationConfig->getDatalayerImplementation()->name,
93                ]
94            );
95            $config->set(
96                'apie.doctrine',
97                [
98                    'build_once' => false,
99                    'run_migrations' => true,
100                    'connection_params' => [
101                        'driver' => 'pdo_sqlite'
102                    ]
103                ]
104            );
105            $config->set('apie.remote_mcp_path', '/mcp');
106            $config->set('apie.enable_graphql', true);
107            $rawConfig = $config->get('apie');
108            $processedConfig = ValidateAndSanitizeConfig::process($rawConfig);
109            $config->set('apie', $processedConfig);
110        });
111    }
112
113    /**
114     * Boot application. Should be called at the start of the test.
115     */
116    public function bootApplication(): void
117    {
118        IntegrationTestLogger::resetLoggedException();
119        $this->setUp();
120        $this->session([]);
121        if (getenv('PHPUNIT_LOG_INTEGRATION_OUTPUT')) {
122            $this->withoutExceptionHandling([
123                NotFoundHttpException::class
124            ]);
125        }
126        $this->app->instance(FileWriterInterface::class, new MockFileWriter());
127        $this->app->instance(
128            AiClient::class,
129            MockFactory::createMockAiClient()
130        );
131        unset($this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME]);
132        $this->app->boot();
133    }
134
135    /**
136     * Gets service container of application. Should be used as little as possible.
137     */
138    public function getServiceContainer(): ContainerInterface
139    {
140        return $this->app;
141    }
142
143    /**
144     * Cleans up application. Should be called at the end of the test.
145     */
146    public function cleanApplication(): void
147    {
148        $this->tearDown();
149    }
150
151    /**
152     * Does a HTTP request on the application and returns the response.
153     */
154    public function httpRequestGet(string $uri): ResponseInterface
155    {
156        //workaround against spacebar search test and Laravel doing unneeded things
157        if (str_ends_with($uri, ' ')) {
158            $uri .= '&dummy=1';
159        }
160        $testResponse = $this->get($uri);
161        $laravelResponse = $testResponse->baseResponse;
162        return $this->handleResponse($laravelResponse);
163    }
164
165    private function handleResponse(HttpFoundationResponse $laravelResponse): ResponseInterface
166    {
167        $cookie = $laravelResponse->headers->getCookies(
168            ResponseHeaderBag::COOKIES_ARRAY
169        )[""]["/"][AddAuthenticationCookie::COOKIE_NAME] ?? null;
170        if ($cookie !== null) {
171            $cookie = Cookie::fromString($cookie)->getValue();
172        }
173        if ($cookie) {
174            $this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME] = $cookie;
175        } else {
176            unset($this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME]);
177        }
178
179        $psrFactory = new NyholmPsr17Factory();
180        $factory = new PsrHttpFactory($psrFactory, $psrFactory, $psrFactory, $psrFactory);
181        return $factory->createResponse($laravelResponse);
182    }
183
184    protected function getPackageProviders($app): array
185    {
186        return [ApieServiceProvider::class];
187    }
188
189    public function httpRequest(TestRequestInterface $testRequest): ResponseInterface
190    {
191        $psrRequest = $testRequest->getRequest();
192        $factory = new HttpFoundationFactory();
193        $sfRequest = $factory->createRequest($psrRequest);
194        $laravelRequest = Request::createFromBase($sfRequest);
195        if (isset($this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME])) {
196            $laravelRequest->cookies->set(AddAuthenticationCookie::COOKIE_NAME, $this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME]);
197        }
198        $laravelResponse = $this->app->make(HttpKernel::class)->handle($laravelRequest);
199        return $this->handleResponse($laravelResponse);
200    }
201
202    public function loginAs(DecryptedAuthenticatedUser $user): void
203    {
204        $textEncrypter = new TextEncrypter('test');
205        $this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME] = $textEncrypter->encrypt($user->toNative());
206    }
207
208    /**
209     * Forget that you are logged in.
210     */
211    public function logout(): void
212    {
213        Auth::logout();
214        unset($this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME]);
215    }
216
217    public function getLoggedInAs(): ?DecryptedAuthenticatedUser
218    {
219        if (empty($this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME])) {
220            return null;
221        }
222        $textEncrypter = new TextEncrypter('test');
223        return DecryptedAuthenticatedUser::fromNative(
224            $textEncrypter->decrypt($this->defaultCookies[AddAuthenticationCookie::COOKIE_NAME]),
225        );
226    }
227}