Skip to content

Commit 71eade7

Browse files
luispabonclaude
andauthored
Expand test suite: Behat zip inspection, functional, and unit tests (#387)
* Expand Behat behaviour test suite with zip inspection and per-service coverage - Add zip-inspection step definitions to DefaultContext (contain/not-contain file and content checks, AfterScenario cleanup) - Add container_for_mariadb_* and container_for_postgres_* wrapper divs to generator template to support validation error selectors - Expand generator.feature from 6 to 20 scenarios covering zip structure, PHP version in Dockerfile, MariaDB/PostgreSQL validation, per-service docker-compose content (including credentials), optional services absent by default, and all services enabled simultaneously Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add a whole bunch of new unit test cases * Add a whole bunch of new functional test cases * Fix old config throwing errors on phpstan --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 853604a commit 71eade7

File tree

17 files changed

+1018
-19
lines changed

17 files changed

+1018
-19
lines changed

config/services.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,15 @@ services:
2626
App\Controller\:
2727
resource: '../src/Controller/'
2828
tags: [ 'controller.service_arguments' ]
29+
bind:
30+
$environment: '%kernel.environment%'
2931

3032
# add more service definitions when explicit configuration is needed
3133
# please note that last definitions always *replace* previous ones
3234

35+
App\PHPDocker\Zip\Archiver:
36+
shared: false
37+
3338
# Instantiate some third party libraries we need for autowiring our own services
3439
Michelf\MarkdownExtra: ~
3540
Symfony\Component\Yaml\Dumper: ~

features/generator.feature

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,38 @@ Feature:
2020
When I press "Generate project archive"
2121
Then the response code should be 200
2222
And I should receive a zip file named "phpdocker.zip"
23-
# And show last response
23+
24+
Scenario: Default zip contains all expected files
25+
Given I am on "/"
26+
When I press "Generate project archive"
27+
Then I should receive a zip file named "phpdocker.zip"
28+
And the zip should contain the file "docker-compose.yml"
29+
And the zip should contain the file "phpdocker/php-fpm/Dockerfile"
30+
And the zip should contain the file "phpdocker/php-fpm/php-ini-overrides.ini"
31+
And the zip should contain the file "phpdocker/nginx/nginx.conf"
32+
And the zip should contain the file "phpdocker/README.md"
33+
And the zip should contain the file "phpdocker/README.html"
34+
35+
Scenario: Webserver and php-fpm are always present in docker-compose.yml
36+
Given I am on "/"
37+
When I press "Generate project archive"
38+
Then I should receive a zip file named "phpdocker.zip"
39+
And the zip file "docker-compose.yml" should contain "webserver:"
40+
And the zip file "docker-compose.yml" should contain "php-fpm:"
41+
42+
Scenario: PHP 8.2 is reflected in Dockerfile
43+
Given I am on "/"
44+
When I select "8.2" from "project_phpOptions_version"
45+
And I press "Generate project archive"
46+
Then I should receive a zip file named "phpdocker.zip"
47+
And the zip file "phpdocker/php-fpm/Dockerfile" should contain "phpdockerio/php:8.2-fpm"
48+
49+
Scenario: PHP 8.5 is reflected in Dockerfile
50+
Given I am on "/"
51+
When I select "8.5" from "project_phpOptions_version"
52+
And I press "Generate project archive"
53+
Then I should receive a zip file named "phpdocker.zip"
54+
And the zip file "phpdocker/php-fpm/Dockerfile" should contain "phpdockerio/php:8.5-fpm"
2455

2556
Scenario: Check MySQL validation works
2657
Given I am on "/"
@@ -31,6 +62,23 @@ Feature:
3162
And the "#container_for_mysql_username" element should contain "This value should not be blank."
3263
And the "#container_for_mysql_password" element should contain "This value should not be blank."
3364

65+
Scenario: Check MariaDB validation works
66+
Given I am on "/"
67+
When I check "MariaDB"
68+
And I press "Generate project archive"
69+
Then the "#container_for_mariadb_rootPassword" element should contain "This value should not be blank."
70+
And the "#container_for_mariadb_databaseName" element should contain "This value should not be blank."
71+
And the "#container_for_mariadb_username" element should contain "This value should not be blank."
72+
And the "#container_for_mariadb_password" element should contain "This value should not be blank."
73+
74+
Scenario: Check PostgreSQL validation works
75+
Given I am on "/"
76+
When I check "Postgres"
77+
And I press "Generate project archive"
78+
Then the "#container_for_postgres_rootUser" element should contain "This value should not be blank."
79+
And the "#container_for_postgres_rootPassword" element should contain "This value should not be blank."
80+
And the "#container_for_postgres_databaseName" element should contain "This value should not be blank."
81+
3482
Scenario: MySQL config works correctly
3583
Given I am on "/"
3684
When I check "MySQL"
@@ -41,3 +89,116 @@ Feature:
4189
When I press "Generate project archive"
4290
Then the response code should be 200
4391
And I should receive a zip file named "phpdocker.zip"
92+
And the zip file "docker-compose.yml" should contain "mysql:"
93+
And the zip file "docker-compose.yml" should contain "MYSQL_ROOT_PASSWORD=root pass"
94+
And the zip file "docker-compose.yml" should contain "MYSQL_DATABASE=db name"
95+
And the zip file "docker-compose.yml" should contain "MYSQL_USER=user"
96+
And the zip file "docker-compose.yml" should contain "MYSQL_PASSWORD=pass"
97+
98+
Scenario: MariaDB config works correctly
99+
Given I am on "/"
100+
When I check "MariaDB"
101+
And I fill in "project_mariadbOptions_rootPassword" with "root pass"
102+
And I fill in "project_mariadbOptions_databaseName" with "db name"
103+
And I fill in "project_mariadbOptions_username" with "user"
104+
And I fill in "project_mariadbOptions_password" with "pass"
105+
When I press "Generate project archive"
106+
Then the response code should be 200
107+
And I should receive a zip file named "phpdocker.zip"
108+
And the zip file "docker-compose.yml" should contain "mariadb:"
109+
And the zip file "docker-compose.yml" should contain "MYSQL_ROOT_PASSWORD=root pass"
110+
And the zip file "docker-compose.yml" should contain "MYSQL_DATABASE=db name"
111+
And the zip file "docker-compose.yml" should contain "MYSQL_USER=user"
112+
And the zip file "docker-compose.yml" should contain "MYSQL_PASSWORD=pass"
113+
114+
Scenario: PostgreSQL config works correctly
115+
Given I am on "/"
116+
When I check "Postgres"
117+
And I fill in "project_postgresOptions_rootUser" with "root user"
118+
And I fill in "project_postgresOptions_rootPassword" with "root pass"
119+
And I fill in "project_postgresOptions_databaseName" with "db name"
120+
When I press "Generate project archive"
121+
Then the response code should be 200
122+
And I should receive a zip file named "phpdocker.zip"
123+
And the zip file "docker-compose.yml" should contain "postgres:"
124+
And the zip file "docker-compose.yml" should contain "POSTGRES_USER=root user"
125+
And the zip file "docker-compose.yml" should contain "POSTGRES_PASSWORD=root pass"
126+
And the zip file "docker-compose.yml" should contain "POSTGRES_DB=db name"
127+
128+
Scenario: Redis is included when enabled
129+
Given I am on "/"
130+
When I check "Redis"
131+
And I press "Generate project archive"
132+
Then I should receive a zip file named "phpdocker.zip"
133+
And the zip file "docker-compose.yml" should contain "redis:"
134+
135+
Scenario: Memcached is included when enabled
136+
Given I am on "/"
137+
When I check "Memcached"
138+
And I press "Generate project archive"
139+
Then I should receive a zip file named "phpdocker.zip"
140+
And the zip file "docker-compose.yml" should contain "memcached:"
141+
142+
Scenario: Mailhog is included when enabled
143+
Given I am on "/"
144+
When I check "Mailhog"
145+
And I press "Generate project archive"
146+
Then I should receive a zip file named "phpdocker.zip"
147+
And the zip file "docker-compose.yml" should contain "mailhog:"
148+
149+
Scenario: Clickhouse is included when enabled
150+
Given I am on "/"
151+
When I check "Clickhouse"
152+
And I press "Generate project archive"
153+
Then I should receive a zip file named "phpdocker.zip"
154+
And the zip file "docker-compose.yml" should contain "clickhouse:"
155+
156+
Scenario: Optional services are absent by default
157+
Given I am on "/"
158+
When I press "Generate project archive"
159+
Then I should receive a zip file named "phpdocker.zip"
160+
And the zip file "docker-compose.yml" should not contain "redis:"
161+
And the zip file "docker-compose.yml" should not contain "memcached:"
162+
And the zip file "docker-compose.yml" should not contain "mailhog:"
163+
And the zip file "docker-compose.yml" should not contain "clickhouse:"
164+
And the zip file "docker-compose.yml" should not contain "mysql:"
165+
And the zip file "docker-compose.yml" should not contain "mariadb:"
166+
And the zip file "docker-compose.yml" should not contain "postgres:"
167+
And the zip file "docker-compose.yml" should not contain "elasticsearch:"
168+
169+
Scenario: All optional services enabled simultaneously
170+
Given I am on "/"
171+
When I check "Redis"
172+
And I check "Memcached"
173+
And I check "Mailhog"
174+
And I check "Clickhouse"
175+
And I check "MySQL"
176+
And I fill in "project_mysqlOptions_rootPassword" with "root pass"
177+
And I fill in "project_mysqlOptions_databaseName" with "db name"
178+
And I fill in "project_mysqlOptions_username" with "user"
179+
And I fill in "project_mysqlOptions_password" with "pass"
180+
And I check "MariaDB"
181+
And I fill in "project_mariadbOptions_rootPassword" with "root pass"
182+
And I fill in "project_mariadbOptions_databaseName" with "db name"
183+
And I fill in "project_mariadbOptions_username" with "user"
184+
And I fill in "project_mariadbOptions_password" with "pass"
185+
And I check "Postgres"
186+
And I fill in "project_postgresOptions_rootUser" with "root user"
187+
And I fill in "project_postgresOptions_rootPassword" with "root pass"
188+
And I fill in "project_postgresOptions_databaseName" with "db name"
189+
And I press "Generate project archive"
190+
Then I should receive a zip file named "phpdocker.zip"
191+
And the zip file "docker-compose.yml" should contain "redis:"
192+
And the zip file "docker-compose.yml" should contain "memcached:"
193+
And the zip file "docker-compose.yml" should contain "mailhog:"
194+
And the zip file "docker-compose.yml" should contain "clickhouse:"
195+
And the zip file "docker-compose.yml" should contain "mysql:"
196+
And the zip file "docker-compose.yml" should contain "MYSQL_ROOT_PASSWORD=root pass"
197+
And the zip file "docker-compose.yml" should contain "MYSQL_DATABASE=db name"
198+
And the zip file "docker-compose.yml" should contain "MYSQL_USER=user"
199+
And the zip file "docker-compose.yml" should contain "MYSQL_PASSWORD=pass"
200+
And the zip file "docker-compose.yml" should contain "mariadb:"
201+
And the zip file "docker-compose.yml" should contain "postgres:"
202+
And the zip file "docker-compose.yml" should contain "POSTGRES_USER=root user"
203+
And the zip file "docker-compose.yml" should contain "POSTGRES_PASSWORD=root pass"
204+
And the zip file "docker-compose.yml" should contain "POSTGRES_DB=db name"

phpstan.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ parameters:
66
- src/
77
- tests/
88

9-
checkMissingIterableValueType: false
109
ignoreErrors:
10+
- identifier: missingType.iterableValue
1111
- '#Access to an undefined property Symfony\\Component\\Validator\\Constraint::\$message#'
1212
- '#Method App\\PHPDocker\\Project\\ServiceOptions\\Postgres::getChoices\(\) should return array<string, string> but returns array<int\|string, string>#'

src/Controller/GeneratorController.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
*/
3636
class GeneratorController extends AbstractController
3737
{
38-
public function __construct(private readonly Generator $generator)
39-
{
38+
public function __construct(
39+
private readonly Generator $generator,
40+
private readonly string $environment,
41+
) {
4042
}
4143

4244
/**
@@ -55,12 +57,14 @@ public function create(Request $request): BinaryFileResponse|Response
5557
// Generate zip file with docker project
5658
$zipFile = $this->generator->generate($project);
5759

58-
// Generate file download & cleanup
60+
// Generate file download & cleanup (keep file in test env so functional tests can read it)
5961
$response = new BinaryFileResponse($zipFile->getTmpFilename());
6062
$response
6163
->prepare($request)
62-
->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $zipFile->getFilename())
63-
->deleteFileAfterSend(true);
64+
->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $zipFile->getFilename());
65+
if ($this->environment !== 'test') {
66+
$response->deleteFileAfterSend(true);
67+
}
6468

6569
return $response;
6670
}

templates/generator.html.twig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797
<div id="mariadb-options" class="row">
9898
{% for field in ['version', 'rootPassword', 'databaseName', 'username', 'password'] %}
9999
{% if (attribute(form.mariadbOptions, field) is defined) %}
100-
{{ form_row(attribute(form.mariadbOptions, field)) }}
100+
<div id="container_for_mariadb_{{ field }}">{{ form_row(attribute(form.mariadbOptions, field)) }}</div>
101101
{% endif %}
102102
{% endfor %}
103103
</div>
@@ -108,7 +108,7 @@
108108
<div id="postgres-options" class="row">
109109
{% for field in ['version', 'rootUser', 'rootPassword', 'databaseName'] %}
110110
{% if (attribute(form.postgresOptions, field) is defined) %}
111-
{{ form_row(attribute(form.postgresOptions, field)) }}
111+
<div id="container_for_postgres_{{ field }}">{{ form_row(attribute(form.postgresOptions, field)) }}</div>
112112
{% endif %}
113113
{% endfor %}
114114
</div>

tests/Behat/DefaultContext.php

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010

1111
final class DefaultContext extends MinkContext
1212
{
13+
private ?ZipArchive $lastZip = null;
14+
private ?string $lastZipTmpFile = null;
15+
1316
/**
1417
* @Then /^the response code should be (\d+)$/
1518
*/
@@ -56,20 +59,63 @@ public function iShouldReceiveAZipFileNamed(string $zipFilename)
5659

5760
Assertion::eqArraySubset($headers, $expectedZipHeaders);
5861

59-
Assertion::true($this->isZipFile($response));
62+
$tmpFile = sprintf('%s', tempnam('/tmp', 'zip_test_'));
63+
file_put_contents(filename: $tmpFile, data: $response);
64+
65+
$zip = new ZipArchive();
66+
$result = $zip->open($tmpFile);
67+
68+
Assertion::true($result);
69+
70+
$this->lastZip = $zip;
71+
$this->lastZipTmpFile = $tmpFile;
6072
}
6173

62-
private function isZipFile(string $data): bool
74+
/** @AfterScenario */
75+
public function cleanUpZip(): void
6376
{
64-
$fn = sprintf('%s', tempnam('/tmp', 'zip_test_'));
65-
file_put_contents(filename: $fn, data: $data);
66-
67-
try {
68-
$zipFile = new ZipArchive();
77+
if ($this->lastZip !== null) {
78+
$this->lastZip->close();
79+
$this->lastZip = null;
80+
}
6981

70-
return $zipFile->open($fn);
71-
} finally {
72-
@unlink($fn);
82+
if ($this->lastZipTmpFile !== null) {
83+
@unlink($this->lastZipTmpFile);
84+
$this->lastZipTmpFile = null;
7385
}
7486
}
87+
88+
/**
89+
* @Then /^the zip should contain the file "([^"]*)"$/
90+
*/
91+
public function theZipShouldContainTheFile(string $path): void
92+
{
93+
Assertion::notNull($this->lastZip, 'No zip file available. Did you call "I should receive a zip file named" first?');
94+
Assertion::true(
95+
$this->lastZip->locateName($path) !== false,
96+
sprintf('File "%s" not found in zip archive', $path),
97+
);
98+
}
99+
100+
/**
101+
* @Then /^the zip file "([^"]*)" should contain "([^"]*)"$/
102+
*/
103+
public function theZipFileShouldContain(string $path, string $content): void
104+
{
105+
Assertion::notNull($this->lastZip, 'No zip file available. Did you call "I should receive a zip file named" first?');
106+
$fileContent = $this->lastZip->getFromName($path);
107+
Assertion::string($fileContent, sprintf('File "%s" not found in zip archive', $path));
108+
Assertion::contains($fileContent, $content, sprintf('File "%s" does not contain "%s"', $path, $content));
109+
}
110+
111+
/**
112+
* @Then /^the zip file "([^"]*)" should not contain "([^"]*)"$/
113+
*/
114+
public function theZipFileShouldNotContain(string $path, string $content): void
115+
{
116+
Assertion::notNull($this->lastZip, 'No zip file available. Did you call "I should receive a zip file named" first?');
117+
$fileContent = $this->lastZip->getFromName($path);
118+
Assertion::string($fileContent, sprintf('File "%s" not found in zip archive', $path));
119+
Assertion::notContains($fileContent, $content, sprintf('File "%s" should not contain "%s"', $path, $content));
120+
}
75121
}

0 commit comments

Comments
 (0)