forked from coollabsio/coolify
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApplicationDeploymentCustomBuildCommandTest.php
More file actions
617 lines (474 loc) · 32 KB
/
ApplicationDeploymentCustomBuildCommandTest.php
File metadata and controls
617 lines (474 loc) · 32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
<?php
/**
* Test to verify that custom Docker Compose build commands properly inject flags.
*
* This test suite verifies that when using a custom build command, the system automatically
* injects the -f (compose file path) and --env-file flags to ensure the correct compose file
* is used and build-time environment variables are available during the build process.
*
* The fix ensures that:
* - -f flag with compose file path is automatically injected after 'docker compose'
* - --env-file /artifacts/build-time.env is automatically injected after 'docker compose'
* - Users can still provide their own -f or --env-file flags to override the default behavior
* - Both flags are injected in a single str_replace operation
* - Build arguments are appended when not using build secrets
*/
it('injects --env-file flag into custom build command', function () {
$customCommand = 'docker compose -f ./docker-compose.yaml build';
// Simulate the injection logic from ApplicationDeploymentJob
if (! str_contains($customCommand, '--env-file')) {
$customCommand = str_replace(
'docker compose',
'docker compose --env-file /artifacts/build-time.env',
$customCommand
);
}
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f ./docker-compose.yaml build');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('does not duplicate --env-file flag when already present', function () {
$customCommand = 'docker compose --env-file /custom/.env -f ./docker-compose.yaml build';
// Simulate the injection logic from ApplicationDeploymentJob
if (! str_contains($customCommand, '--env-file')) {
$customCommand = str_replace(
'docker compose',
'docker compose --env-file /artifacts/build-time.env',
$customCommand
);
}
expect($customCommand)->toBe('docker compose --env-file /custom/.env -f ./docker-compose.yaml build');
expect(substr_count($customCommand, '--env-file'))->toBe(1);
});
it('preserves custom build command structure with env-file injection', function () {
$customCommand = 'docker compose -f ./custom/path/docker-compose.prod.yaml build --no-cache';
// Simulate the injection logic from ApplicationDeploymentJob
if (! str_contains($customCommand, '--env-file')) {
$customCommand = str_replace(
'docker compose',
'docker compose --env-file /artifacts/build-time.env',
$customCommand
);
}
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f ./custom/path/docker-compose.prod.yaml build --no-cache');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
expect($customCommand)->toContain('-f ./custom/path/docker-compose.prod.yaml');
expect($customCommand)->toContain('build --no-cache');
});
it('handles multiple docker compose commands in custom build command', function () {
// Edge case: Only the first 'docker compose' should get the env-file flag
$customCommand = 'docker compose -f ./docker-compose.yaml build';
// Simulate the injection logic from ApplicationDeploymentJob
if (! str_contains($customCommand, '--env-file')) {
$customCommand = str_replace(
'docker compose',
'docker compose --env-file /artifacts/build-time.env',
$customCommand
);
}
// Note: str_replace replaces ALL occurrences, which is acceptable in this case
// since you typically only have one 'docker compose' command
expect($customCommand)->toContain('docker compose --env-file /artifacts/build-time.env');
});
it('verifies build args would be appended correctly', function () {
$customCommand = 'docker compose --env-file /artifacts/build-time.env -f ./docker-compose.yaml build';
$buildArgs = collect([
'--build-arg NODE_ENV=production',
'--build-arg API_URL=https://api.example.com',
]);
// Simulate build args appending logic
$buildArgsString = $buildArgs->implode(' ');
$buildArgsString = str_replace("'", "'\\''", $buildArgsString);
$customCommand .= " {$buildArgsString}";
expect($customCommand)->toContain('--build-arg NODE_ENV=production');
expect($customCommand)->toContain('--build-arg API_URL=https://api.example.com');
expect($customCommand)->toBe(
'docker compose --env-file /artifacts/build-time.env -f ./docker-compose.yaml build --build-arg NODE_ENV=production --build-arg API_URL=https://api.example.com'
);
});
it('properly escapes single quotes in build args', function () {
$buildArg = "--build-arg MESSAGE='Hello World'";
// Simulate the escaping logic from ApplicationDeploymentJob
$escapedBuildArg = str_replace("'", "'\\''", $buildArg);
expect($escapedBuildArg)->toBe("--build-arg MESSAGE='\\''Hello World'\\''");
});
it('handles DOCKER_BUILDKIT prefix with env-file injection', function () {
$customCommand = 'docker compose -f ./docker-compose.yaml build';
// Simulate the injection logic from ApplicationDeploymentJob
if (! str_contains($customCommand, '--env-file')) {
$customCommand = str_replace(
'docker compose',
'docker compose --env-file /artifacts/build-time.env',
$customCommand
);
}
// Simulate BuildKit support
$dockerBuildkitSupported = true;
if ($dockerBuildkitSupported) {
$customCommand = "DOCKER_BUILDKIT=1 {$customCommand}";
}
expect($customCommand)->toBe('DOCKER_BUILDKIT=1 docker compose --env-file /artifacts/build-time.env -f ./docker-compose.yaml build');
expect($customCommand)->toStartWith('DOCKER_BUILDKIT=1');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
// Tests for -f flag injection
it('injects -f flag with compose file path into custom build command', function () {
$customCommand = 'docker compose build';
$composeFilePath = '/artifacts/deployment-uuid/backend/docker-compose.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, $composeFilePath, '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose -f /artifacts/deployment-uuid/backend/docker-compose.yaml --env-file /artifacts/build-time.env build');
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/backend/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('does not duplicate -f flag when already present', function () {
$customCommand = 'docker compose -f ./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f ./custom/docker-compose.yaml build');
expect(substr_count($customCommand, ' -f '))->toBe(1);
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('does not duplicate --file flag when already present', function () {
$customCommand = 'docker compose --file ./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env --file ./custom/docker-compose.yaml build');
expect(substr_count($customCommand, '--file '))->toBe(1);
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('injects both -f and --env-file flags in single operation', function () {
$customCommand = 'docker compose build --no-cache';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/app/docker-compose.prod.yaml', '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/app/docker-compose.prod.yaml --env-file /artifacts/build-time.env build --no-cache');
expect($customCommand)->toContain('-f /artifacts/uuid/app/docker-compose.prod.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
expect($customCommand)->toContain('build --no-cache');
});
it('respects user-provided -f and --env-file flags', function () {
$customCommand = 'docker compose -f ./my-compose.yaml --env-file .env build';
// Use the helper function - should not inject anything since both flags are already present
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
expect($customCommand)->toBe('docker compose -f ./my-compose.yaml --env-file .env build');
expect(substr_count($customCommand, ' -f '))->toBe(1);
expect(substr_count($customCommand, '--env-file'))->toBe(1);
});
// Tests for custom start command -f and --env-file injection
it('injects -f and --env-file flags into custom start command', function () {
$customCommand = 'docker compose up -d';
$serverWorkdir = '/var/lib/docker/volumes/coolify-data/_data/applications/app-uuid';
$composeLocation = '/docker-compose.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose -f /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/docker-compose.yaml --env-file /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/.env up -d');
expect($customCommand)->toContain('-f /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/.env');
});
it('does not duplicate -f flag in start command when already present', function () {
$customCommand = 'docker compose -f ./custom-compose.yaml up -d';
$serverWorkdir = '/var/lib/docker/volumes/coolify-data/_data/applications/app-uuid';
$composeLocation = '/docker-compose.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose --env-file /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/.env -f ./custom-compose.yaml up -d');
expect(substr_count($customCommand, ' -f '))->toBe(1);
expect($customCommand)->toContain('--env-file');
});
it('does not duplicate --env-file flag in start command when already present', function () {
$customCommand = 'docker compose --env-file ./my.env up -d';
$serverWorkdir = '/var/lib/docker/volumes/coolify-data/_data/applications/app-uuid';
$composeLocation = '/docker-compose.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose -f /var/lib/docker/volumes/coolify-data/_data/applications/app-uuid/docker-compose.yaml --env-file ./my.env up -d');
expect(substr_count($customCommand, '--env-file'))->toBe(1);
expect($customCommand)->toContain('-f');
});
it('respects both user-provided flags in start command', function () {
$customCommand = 'docker compose -f ./my-compose.yaml --env-file ./.env up -d';
$serverWorkdir = '/var/lib/docker/volumes/coolify-data/_data/applications/app-uuid';
$composeLocation = '/docker-compose.yaml';
// Use the helper function - should not inject anything since both flags are already present
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose -f ./my-compose.yaml --env-file ./.env up -d');
expect(substr_count($customCommand, ' -f '))->toBe(1);
expect(substr_count($customCommand, '--env-file'))->toBe(1);
});
it('injects both flags in start command with additional parameters', function () {
$customCommand = 'docker compose up -d --remove-orphans';
$serverWorkdir = '/workdir/app';
$composeLocation = '/backend/docker-compose.prod.yaml';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, "{$serverWorkdir}{$composeLocation}", "{$serverWorkdir}/.env");
expect($customCommand)->toBe('docker compose -f /workdir/app/backend/docker-compose.prod.yaml --env-file /workdir/app/.env up -d --remove-orphans');
expect($customCommand)->toContain('-f /workdir/app/backend/docker-compose.prod.yaml');
expect($customCommand)->toContain('--env-file /workdir/app/.env');
expect($customCommand)->toContain('--remove-orphans');
});
// Security tests: Prevent bypass vectors for flag detection
it('detects -f flag with equals sign format (bypass vector)', function () {
$customCommand = 'docker compose -f=./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f= is already present
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f=./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --file flag with equals sign format (bypass vector)', function () {
$customCommand = 'docker compose --file=./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since --file= is already present
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env --file=./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --env-file flag with equals sign format (bypass vector)', function () {
$customCommand = 'docker compose --env-file=./custom/.env build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject --env-file flag since --env-file= is already present
expect($customCommand)->toBe('docker compose -f /artifacts/deployment-uuid/docker-compose.yaml --env-file=./custom/.env build');
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->not->toContain('--env-file /artifacts/build-time.env');
});
it('detects -f flag with tab character whitespace (bypass vector)', function () {
$customCommand = "docker compose\t-f\t./custom/docker-compose.yaml build";
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f with tab is already present
expect($customCommand)->toBe("docker compose --env-file /artifacts/build-time.env\t-f\t./custom/docker-compose.yaml build");
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --env-file flag with tab character whitespace (bypass vector)', function () {
$customCommand = "docker compose\t--env-file\t./custom/.env build";
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject --env-file flag since --env-file with tab is already present
expect($customCommand)->toBe("docker compose -f /artifacts/deployment-uuid/docker-compose.yaml\t--env-file\t./custom/.env build");
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->not->toContain('--env-file /artifacts/build-time.env');
});
it('detects -f flag with multiple spaces (bypass vector)', function () {
$customCommand = 'docker compose -f ./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f with multiple spaces is already present
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f ./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --file flag with multiple spaces (bypass vector)', function () {
$customCommand = 'docker compose --file ./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since --file with multiple spaces is already present
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env --file ./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects -f flag at start of command (edge case)', function () {
$customCommand = '-f ./custom/docker-compose.yaml docker compose build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f is at start of command
expect($customCommand)->toBe('-f ./custom/docker-compose.yaml docker compose --env-file /artifacts/build-time.env build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects --env-file flag at start of command (edge case)', function () {
$customCommand = '--env-file=./custom/.env docker compose build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject --env-file flag since --env-file is at start of command
expect($customCommand)->toBe('--env-file=./custom/.env docker compose -f /artifacts/deployment-uuid/docker-compose.yaml build');
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->not->toContain('--env-file /artifacts/build-time.env');
});
it('handles mixed whitespace correctly (comprehensive test)', function () {
$customCommand = "docker compose\t-f=./custom/docker-compose.yaml --env-file\t./custom/.env build";
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject any flags since both are already present with various whitespace
expect($customCommand)->toBe("docker compose\t-f=./custom/docker-compose.yaml --env-file\t./custom/.env build");
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->not->toContain('--env-file /artifacts/build-time.env');
});
// Tests for concatenated -f flag format (no space, no equals)
it('detects -f flag in concatenated format -fvalue (bypass vector)', function () {
$customCommand = 'docker compose -f./custom/docker-compose.yaml build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f is concatenated with value
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f./custom/docker-compose.yaml build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
it('detects -f flag concatenated with path containing slash', function () {
$customCommand = 'docker compose -f/path/to/compose.yml up -d';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f is concatenated
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f/path/to/compose.yml up -d');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('-f/path/to/compose.yml');
});
it('detects -f flag concatenated at start of command', function () {
$customCommand = '-f./compose.yaml docker compose build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag since -f is already present (even at start)
expect($customCommand)->toBe('-f./compose.yaml docker compose --env-file /artifacts/build-time.env build');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
});
it('detects concatenated -f flag with relative path', function () {
$customCommand = 'docker compose -f../docker-compose.prod.yaml build --no-cache';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Should NOT inject -f flag
expect($customCommand)->toBe('docker compose --env-file /artifacts/build-time.env -f../docker-compose.prod.yaml build --no-cache');
expect($customCommand)->not->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('-f../docker-compose.prod.yaml');
});
it('correctly injects when no -f flag is present (sanity check after concatenated fix)', function () {
$customCommand = 'docker compose build --no-cache';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/deployment-uuid/docker-compose.yaml', '/artifacts/build-time.env');
// SHOULD inject both flags
expect($customCommand)->toBe('docker compose -f /artifacts/deployment-uuid/docker-compose.yaml --env-file /artifacts/build-time.env build --no-cache');
expect($customCommand)->toContain('-f /artifacts/deployment-uuid/docker-compose.yaml');
expect($customCommand)->toContain('--env-file /artifacts/build-time.env');
});
// Edge case tests: First occurrence only replacement
it('only replaces first docker compose occurrence in chained commands', function () {
$customCommand = 'docker compose pull && docker compose build';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Only the FIRST 'docker compose' should get the flags
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env pull && docker compose build');
expect($customCommand)->toContain('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env pull');
expect($customCommand)->toContain(' && docker compose build');
// Verify the second occurrence is NOT modified
expect(substr_count($customCommand, '-f /artifacts/uuid/docker-compose.yaml'))->toBe(1);
expect(substr_count($customCommand, '--env-file /artifacts/build-time.env'))->toBe(1);
});
it('does not modify docker compose string in echo statements', function () {
$customCommand = 'docker compose build && echo "docker compose finished successfully"';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Only the FIRST 'docker compose' (the command) should get flags, NOT the echo message
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build && echo "docker compose finished successfully"');
expect($customCommand)->toContain('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build');
expect($customCommand)->toContain('echo "docker compose finished successfully"');
// Verify echo message is NOT modified
expect(substr_count($customCommand, 'docker compose', 0))->toBe(2); // Two total occurrences
expect(substr_count($customCommand, '-f /artifacts/uuid/docker-compose.yaml'))->toBe(1); // Only first has flags
});
it('does not modify docker compose string in bash comments', function () {
$customCommand = 'docker compose build # This runs docker compose to build the image';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// Only the FIRST 'docker compose' (the command) should get flags, NOT the comment
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build # This runs docker compose to build the image');
expect($customCommand)->toContain('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build');
expect($customCommand)->toContain('# This runs docker compose to build the image');
// Verify comment is NOT modified
expect(substr_count($customCommand, 'docker compose', 0))->toBe(2); // Two total occurrences
expect(substr_count($customCommand, '-f /artifacts/uuid/docker-compose.yaml'))->toBe(1); // Only first has flags
});
// False positive prevention tests: Flags like -foo, -from, -feature should NOT be detected as -f
it('injects -f flag when command contains -foo flag (not -f)', function () {
$customCommand = 'docker compose build --foo bar';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// SHOULD inject -f flag because -foo is NOT the -f flag
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build --foo bar');
expect($customCommand)->toContain('-f /artifacts/uuid/docker-compose.yaml');
});
it('injects -f flag when command contains --from flag (not -f)', function () {
$customCommand = 'docker compose build --from cache-image';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// SHOULD inject -f flag because --from is NOT the -f flag
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build --from cache-image');
expect($customCommand)->toContain('-f /artifacts/uuid/docker-compose.yaml');
});
it('injects -f flag when command contains -feature flag (not -f)', function () {
$customCommand = 'docker compose build -feature test';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// SHOULD inject -f flag because -feature is NOT the -f flag
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build -feature test');
expect($customCommand)->toContain('-f /artifacts/uuid/docker-compose.yaml');
});
it('injects -f flag when command contains -fast flag (not -f)', function () {
$customCommand = 'docker compose build -fast';
// Use the helper function
$customCommand = injectDockerComposeFlags($customCommand, '/artifacts/uuid/docker-compose.yaml', '/artifacts/build-time.env');
// SHOULD inject -f flag because -fast is NOT the -f flag
expect($customCommand)->toBe('docker compose -f /artifacts/uuid/docker-compose.yaml --env-file /artifacts/build-time.env build -fast');
expect($customCommand)->toContain('-f /artifacts/uuid/docker-compose.yaml');
});
// Path normalization tests for preview methods
it('normalizes path when baseDirectory is root slash', function () {
$baseDirectory = '/';
$composeLocation = '/docker-compose.yaml';
// Normalize baseDirectory to prevent double slashes
$normalizedBase = $baseDirectory === '/' ? '' : rtrim($baseDirectory, '/');
$path = ".{$normalizedBase}{$composeLocation}";
expect($path)->toBe('./docker-compose.yaml');
expect($path)->not->toContain('//');
});
it('normalizes path when baseDirectory has trailing slash', function () {
$baseDirectory = '/backend/';
$composeLocation = '/docker-compose.yaml';
// Normalize baseDirectory to prevent double slashes
$normalizedBase = $baseDirectory === '/' ? '' : rtrim($baseDirectory, '/');
$path = ".{$normalizedBase}{$composeLocation}";
expect($path)->toBe('./backend/docker-compose.yaml');
expect($path)->not->toContain('//');
});
it('handles empty baseDirectory correctly', function () {
$baseDirectory = '';
$composeLocation = '/docker-compose.yaml';
// Normalize baseDirectory to prevent double slashes
$normalizedBase = $baseDirectory === '/' ? '' : rtrim($baseDirectory, '/');
$path = ".{$normalizedBase}{$composeLocation}";
expect($path)->toBe('./docker-compose.yaml');
expect($path)->not->toContain('//');
});
it('handles normal baseDirectory without trailing slash', function () {
$baseDirectory = '/backend';
$composeLocation = '/docker-compose.yaml';
// Normalize baseDirectory to prevent double slashes
$normalizedBase = $baseDirectory === '/' ? '' : rtrim($baseDirectory, '/');
$path = ".{$normalizedBase}{$composeLocation}";
expect($path)->toBe('./backend/docker-compose.yaml');
expect($path)->not->toContain('//');
});
it('handles nested baseDirectory with trailing slash', function () {
$baseDirectory = '/app/backend/';
$composeLocation = '/docker-compose.prod.yaml';
// Normalize baseDirectory to prevent double slashes
$normalizedBase = $baseDirectory === '/' ? '' : rtrim($baseDirectory, '/');
$path = ".{$normalizedBase}{$composeLocation}";
expect($path)->toBe('./app/backend/docker-compose.prod.yaml');
expect($path)->not->toContain('//');
});
it('produces correct preview path with normalized baseDirectory', function () {
$testCases = [
['baseDir' => '/', 'compose' => '/docker-compose.yaml', 'expected' => './docker-compose.yaml'],
['baseDir' => '', 'compose' => '/docker-compose.yaml', 'expected' => './docker-compose.yaml'],
['baseDir' => '/backend', 'compose' => '/docker-compose.yaml', 'expected' => './backend/docker-compose.yaml'],
['baseDir' => '/backend/', 'compose' => '/docker-compose.yaml', 'expected' => './backend/docker-compose.yaml'],
['baseDir' => '/app/src/', 'compose' => '/docker-compose.prod.yaml', 'expected' => './app/src/docker-compose.prod.yaml'],
];
foreach ($testCases as $case) {
$normalizedBase = $case['baseDir'] === '/' ? '' : rtrim($case['baseDir'], '/');
$path = ".{$normalizedBase}{$case['compose']}";
expect($path)->toBe($case['expected'], "Failed for baseDir: {$case['baseDir']}");
expect($path)->not->toContain('//', "Double slash found for baseDir: {$case['baseDir']}");
}
});