Skip to content

Commit dc4a575

Browse files
committed
Db pull test added
1 parent b4f9ce9 commit dc4a575

3 files changed

Lines changed: 310 additions & 2 deletions

File tree

console/DbPull.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function handle(): int
3131

3232
try {
3333
$this->line("Connecting to remote server '{$serverName}'...");
34-
$executor = new RemoteExecutor($serverName);
34+
$executor = $this->createExecutor($serverName);
3535
$remoteConfig = $executor->config['database'];
3636
$remoteTempFile = rtrim($executor->config['path'], '/') . '/' . $fileName . ($useGzip ? '.gz' : '');
3737

@@ -110,4 +110,9 @@ public function handle(): int
110110

111111
return self::SUCCESS;
112112
}
113+
114+
protected function createExecutor(string $server): RemoteExecutor
115+
{
116+
return new RemoteExecutor($server);
117+
}
113118
}

tests/console/DbPullTest.php

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
<?php namespace NumenCode\SyncOps\Tests\Console;
2+
3+
use Mockery;
4+
use Carbon\Carbon;
5+
use PluginTestCase;
6+
use NumenCode\SyncOps\Console\DbPull;
7+
use NumenCode\SyncOps\Classes\SshExecutor;
8+
use NumenCode\SyncOps\Classes\SftpExecutor;
9+
use NumenCode\SyncOps\Classes\RemoteExecutor;
10+
11+
class RemoteExecutorStubForDbPull extends RemoteExecutor
12+
{
13+
public function __construct()
14+
{
15+
/* bypass parent */
16+
}
17+
}
18+
19+
class DbPullTestHelper extends DbPull
20+
{
21+
// Expose protected methods as public for testing/mocking
22+
public function createExecutor(string $server): RemoteExecutor
23+
{
24+
return parent::createExecutor($server);
25+
}
26+
27+
public function runLocalCommand(string $command, int $timeout = 60): string
28+
{
29+
return parent::runLocalCommand($command, $timeout);
30+
}
31+
}
32+
33+
class DbPullTest extends PluginTestCase
34+
{
35+
protected string $timestamp = '2024-01-02_03_04_05';
36+
37+
public function setUp(): void
38+
{
39+
parent::setUp();
40+
41+
// Ensure console command binding exists (AFTER parent::setUp(), when app is available)
42+
if (!$this->app->bound('command.syncops.db_pull')) {
43+
$this->app->bind('command.syncops.db_pull', function () {
44+
return new class extends \Illuminate\Console\Command {
45+
protected $signature = 'syncops:db-pull';
46+
public function handle(): int
47+
{
48+
return 0;
49+
}
50+
};
51+
});
52+
}
53+
54+
// Stable timestamp for predictable filenames
55+
Carbon::setTestNow(Carbon::create(2024, 1, 2, 3, 4, 5));
56+
57+
// Local DB config used by import
58+
config()->set('database.default', 'mysql');
59+
config()->set('database.connections.mysql', [
60+
'username' => 'localuser',
61+
'password' => 'localpass',
62+
'database' => 'mydb',
63+
]);
64+
}
65+
66+
public function tearDown(): void
67+
{
68+
// Attempt cleanup of created files
69+
$paths = [
70+
base_path($this->timestamp . '.sql.gz'),
71+
base_path($this->timestamp . '.sql'),
72+
];
73+
74+
foreach ($paths as $p) {
75+
if (is_file($p)) {
76+
@unlink($p);
77+
}
78+
}
79+
80+
Mockery::close();
81+
parent::tearDown();
82+
}
83+
84+
/**
85+
* Test function: handle
86+
* Gzip + import flow: creates remote dump, downloads .gz, unzips, imports locally, cleans up.
87+
*/
88+
public function testHandleWithGzipAndImportSuccess(): void
89+
{
90+
$server = 'staging';
91+
$remotePath = '/var/www/app';
92+
$remoteDumpGz = $remotePath . '/' . $this->timestamp . '.sql.gz';
93+
$localGz = base_path($this->timestamp . '.sql.gz');
94+
$localSql = base_path($this->timestamp . '.sql');
95+
96+
// Build executor stub with config expected by DbPull
97+
$executor = new RemoteExecutorStubForDbPull();
98+
$executor->config = [
99+
'path' => $remotePath,
100+
'database' => [
101+
'username' => 'ruser',
102+
'password' => 'rpass',
103+
'database' => 'rdb',
104+
'tables' => ['table1', 'table2'],
105+
],
106+
];
107+
108+
// SSH expectations: dump command and cleanup rm -f $ssh
109+
$ssh = Mockery::mock(SshExecutor::class);
110+
$ssh->shouldReceive('runRawCommand')
111+
->once()
112+
->with(Mockery::on(function ($cmd) use ($remoteDumpGz) {
113+
return is_string($cmd)
114+
&& str_contains($cmd, 'mysqldump')
115+
&& str_contains($cmd, $remoteDumpGz)
116+
&& str_contains($cmd, 'table1')
117+
&& str_contains($cmd, 'table2');
118+
}))
119+
->andReturn('dump ok');
120+
121+
// Cleanup: rm -f remote file
122+
$ssh->shouldReceive('runRawCommand')
123+
->once()
124+
->with(Mockery::on(function ($cmd) use ($remoteDumpGz) {
125+
return str_starts_with($cmd, 'rm -f') && str_contains($cmd, basename($remoteDumpGz));
126+
}))
127+
->andReturn('');
128+
129+
$executor->ssh = $ssh;
130+
131+
// SFTP download will create the gz file locally
132+
$sftp = Mockery::mock(SftpExecutor::class);
133+
$sftp->shouldReceive('download')
134+
->once()
135+
->with($remoteDumpGz, $localGz)
136+
->andReturnUsing(function ($remote, $local) {
137+
$gz = gzopen($local, 'wb9');
138+
gzwrite($gz, "-- SQL DUMP --\nCREATE TABLE t(id INT);\n");
139+
gzclose($gz);
140+
return null;
141+
});
142+
143+
$executor->sftp = $sftp;
144+
145+
// Partial mock the command (using helper subclass to expose methods)
146+
$cmd = Mockery::mock(DbPullTestHelper::class)->makePartial();
147+
$cmd->shouldReceive('argument')->with('server')->andReturn($server);
148+
$cmd->shouldReceive('option')->with('timestamp')->andReturn('Y-m-d_H_i_s');
149+
$cmd->shouldReceive('option')->with('no-gzip')->andReturnNull(); // gzip enabled
150+
$cmd->shouldReceive('option')->with('no-import')->andReturnNull(); // import enabled
151+
152+
// Allow console noise
153+
$cmd->shouldReceive('newLine')->atLeast()->once();
154+
$cmd->shouldReceive('line')->atLeast()->once();
155+
$cmd->shouldReceive('comment')->atLeast()->once();
156+
$cmd->shouldReceive('info')->atLeast()->once();
157+
$cmd->shouldReceive('error')->zeroOrMoreTimes();
158+
159+
// Replace executor creation
160+
$cmd->shouldReceive('createExecutor')->once()->with($server)->andReturn($executor);
161+
162+
// Expect local import to be executed with a command string containing mysql and local SQL basename.
163+
// Relaxed matcher: test for 'mysql ' and the basename of the SQL file to be robust against quoting/escaping.
164+
$cmd->shouldReceive('runLocalCommand')
165+
->once()
166+
->with(Mockery::on(function ($importCmd) use ($localSql) {
167+
return is_string($importCmd)
168+
&& str_contains($importCmd, 'mysql ')
169+
&& str_contains($importCmd, basename($localSql));
170+
}))
171+
->andReturn('');
172+
173+
$result = $cmd->handle();
174+
$this->assertSame(DbPull::SUCCESS, $result);
175+
176+
// Gz should have been removed after unzip; SQL should have been removed after import cleanup
177+
$this->assertFileDoesNotExist($localGz);
178+
$this->assertFileDoesNotExist($localSql);
179+
}
180+
181+
/**
182+
* Test function: handle
183+
* No-gzip + no-import: downloads plain .sql and keeps it locally; import not executed.
184+
*/
185+
public function testHandleWithoutGzipAndNoImportKeepsLocalFile(): void
186+
{
187+
$server = 'prod';
188+
$remotePath = '/srv/site';
189+
$remoteDump = $remotePath . '/' . $this->timestamp . '.sql';
190+
$localSql = base_path($this->timestamp . '.sql');
191+
192+
$executor = new RemoteExecutorStubForDbPull();
193+
$executor->config = [
194+
'path' => $remotePath,
195+
'database' => [
196+
'username' => 'ruser',
197+
'password' => 'rpass',
198+
'database' => 'rdb',
199+
'tables' => [],
200+
],
201+
];
202+
203+
$ssh = Mockery::mock(SshExecutor::class);
204+
$ssh->shouldReceive('runRawCommand')->once()->with(Mockery::on(function ($cmd) use ($remoteDump) {
205+
return str_contains($cmd, 'mysqldump') && str_contains($cmd, $remoteDump);
206+
}))->andReturn('dump ok');
207+
208+
// Cleanup of remote temp
209+
$ssh->shouldReceive('runRawCommand')->once()->with(Mockery::on(function ($cmd) use ($remoteDump) {
210+
return str_starts_with($cmd, 'rm -f') && str_contains($cmd, basename($remoteDump));
211+
}))->andReturn('');
212+
213+
$executor->ssh = $ssh;
214+
215+
$sftp = Mockery::mock(SftpExecutor::class);
216+
$sftp->shouldReceive('download')->once()->with($remoteDump, $localSql)
217+
->andReturnUsing(function ($remote, $local) {
218+
file_put_contents($local, "-- SQL --\n");
219+
return null;
220+
});
221+
222+
$executor->sftp = $sftp;
223+
224+
$cmd = Mockery::mock(DbPullTestHelper::class)->makePartial();
225+
$cmd->shouldReceive('argument')->with('server')->andReturn($server);
226+
$cmd->shouldReceive('option')->with('timestamp')->andReturn('Y-m-d_H_i_s');
227+
$cmd->shouldReceive('option')->with('no-gzip')->andReturnTrue();
228+
$cmd->shouldReceive('option')->with('no-import')->andReturnTrue();
229+
$cmd->shouldReceive('createExecutor')->once()->with($server)->andReturn($executor);
230+
231+
// Import should not be called
232+
$cmd->shouldNotReceive('runLocalCommand');
233+
234+
// Console outputs
235+
$cmd->shouldReceive('newLine')->atLeast()->once();
236+
$cmd->shouldReceive('line')->atLeast()->once();
237+
$cmd->shouldReceive('comment')->atLeast()->once();
238+
$cmd->shouldReceive('info')->atLeast()->once();
239+
240+
$result = $cmd->handle();
241+
$this->assertSame(DbPull::SUCCESS, $result);
242+
$this->assertFileExists($localSql);
243+
}
244+
245+
/**
246+
* Test function: handle
247+
* If remote dump fails (exception), the command returns FAILURE and still attempts cleanup.
248+
*/
249+
public function testHandleRemoteErrorReturnsFailureAndCleansUp(): void
250+
{
251+
$server = 'error-srv';
252+
$remotePath = '/data/app';
253+
$remoteDumpGz = $remotePath . '/' . $this->timestamp . '.sql.gz';
254+
255+
$executor = new RemoteExecutorStubForDbPull();
256+
$executor->config = [
257+
'path' => $remotePath,
258+
'database' => [
259+
'username' => 'ruser',
260+
'password' => 'rpass',
261+
'database' => 'rdb',
262+
'tables' => ['a'],
263+
],
264+
];
265+
266+
$ssh = Mockery::mock(SshExecutor::class);
267+
// Throw on dump
268+
$ssh->shouldReceive('runRawCommand')->once()->andThrow(new \RuntimeException('remote dump failed'));
269+
270+
// Cleanup still attempted (rm -f ...)
271+
$ssh->shouldReceive('runRawCommand')->once()->with(Mockery::on(function ($cmd) use ($remoteDumpGz) {
272+
return str_starts_with($cmd, 'rm -f') && str_contains($cmd, basename($remoteDumpGz));
273+
}))->andReturn('');
274+
275+
$executor->ssh = $ssh;
276+
277+
// SFTP should not be called in this path, but safe to provide a mock
278+
$executor->sftp = Mockery::mock(SftpExecutor::class);
279+
280+
$cmd = Mockery::mock(DbPullTestHelper::class)->makePartial();
281+
$cmd->shouldReceive('argument')->with('server')->andReturn($server);
282+
$cmd->shouldReceive('option')->with('timestamp')->andReturn('Y-m-d_H_i_s');
283+
$cmd->shouldReceive('option')->with('no-gzip')->andReturnNull();
284+
$cmd->shouldReceive('option')->with('no-import')->andReturnNull();
285+
$cmd->shouldReceive('createExecutor')->once()->with($server)->andReturn($executor);
286+
287+
// Console outputs
288+
$cmd->shouldReceive('newLine')->atLeast()->once();
289+
$cmd->shouldReceive('line')->atLeast()->once();
290+
$cmd->shouldReceive('comment')->zeroOrMoreTimes();
291+
$cmd->shouldReceive('error')->atLeast()->once();
292+
293+
$result = $cmd->handle();
294+
$this->assertSame(DbPull::FAILURE, $result);
295+
296+
// No local files should exist
297+
$this->assertFileDoesNotExist(base_path($this->timestamp . '.sql'));
298+
$this->assertFileDoesNotExist(base_path($this->timestamp . '.sql.gz'));
299+
}
300+
}

tests/console/ProjectDeployTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
class RemoteExecutorStub extends RemoteExecutor
1010
{
11-
public function __construct() { /* bypass parent */ }
11+
public function __construct()
12+
{
13+
/* bypass parent */
14+
}
1215
}
1316

1417
class ProjectDeployTest extends PluginTestCase

0 commit comments

Comments
 (0)