diff --git a/app/Http/Controllers/SubmissionController.php b/app/Http/Controllers/SubmissionController.php
index 4e0785a24d..8a869f6ffa 100644
--- a/app/Http/Controllers/SubmissionController.php
+++ b/app/Http/Controllers/SubmissionController.php
@@ -17,14 +17,12 @@
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
-use Illuminate\Support\Facades\Artisan;
-use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
use League\Flysystem\UnableToMoveFile;
use League\Flysystem\UnableToReadFile;
-use League\Flysystem\UnableToWriteFile;
+use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -73,6 +71,7 @@ private function failProcessing(?string $filename, int $outResponseCode, string
}
/**
+ * @throws BadRequestException
* @throws BadSubmissionException
*/
private function submitProcess(): Response
@@ -83,7 +82,7 @@ private function submitProcess(): Response
$responseMessage = '';
$projectname = request()->string('project', '');
- if (strlen($projectname) === 0) {
+ if (!request()->filled('project')) {
Log::info('Rejected submission with no project name');
$this->failProcessing(null, Response::HTTP_BAD_REQUEST, 'No project name provided.');
}
@@ -99,41 +98,32 @@ private function submitProcess(): Response
$this->failProcessing(null, Response::HTTP_BAD_REQUEST, "Invalid project name: $projectname");
}
- $expected_md5 = request()->query('MD5', '');
- if ($expected_md5 !== '' && !preg_match('/^[a-f0-9]{32}$/i', $expected_md5)) {
+ $expected_md5 = request()->query->getString('MD5');
+ if (request()->filled('MD5') && !SubmissionUtils::isValidMD5($expected_md5)) {
Log::info("Rejected submission with invalid hash '$expected_md5' for project $projectname");
$this->failProcessing(null, Response::HTTP_BAD_REQUEST, "Provided md5 hash '{$expected_md5}' is improperly formatted.");
}
// Get auth token (if any).
- $authtoken = request()->bearerToken();
- $authtoken_hash = $authtoken === null || $authtoken === '' ? '' : AuthTokenService::hash($authtoken);
+ $authtoken_hash = AuthTokenService::hash(request()->bearerToken());
// Check that the md5sum of the file matches what we were told to expect.
$fp = request()->getContent(true);
- if (strlen($expected_md5) > 0) {
+ if (request()->filled('MD5') && !SubmissionUtils::isFileMD5Correct($fp, $expected_md5)) {
$md5sum = SubmissionUtils::hashFileHandle($fp, 'md5');
- if ($md5sum !== $expected_md5) {
- Log::info("Rejected submission because hash '$md5sum' does not match the expected hash '$expected_md5' for project $projectname");
- $this->failProcessing(null, Response::HTTP_BAD_REQUEST, "md5 mismatch. expected: {$expected_md5}, received: {$md5sum}");
- }
+ Log::info("Rejected submission because hash '$md5sum' does not match the expected hash '$expected_md5' for project $projectname");
+ $this->failProcessing(null, Response::HTTP_BAD_REQUEST, "md5 mismatch. expected: {$expected_md5}, received: {$md5sum}");
}
// Save the incoming file in the inbox directory.
$filename = SubmissionUtils::xmlFilename((string) $projectname, $authtoken_hash, $expected_md5);
- try {
- Storage::put("inbox/{$filename}", $fp);
- } catch (UnableToWriteFile $e) {
- report($e);
+ if (!SubmissionUtils::storeInInbox($filename, $fp)) {
$this->failProcessing($filename, Response::HTTP_INTERNAL_SERVER_ERROR, 'Failed to save submission file.');
}
// Check if we can connect to the database before proceeding any further.
if (!SystemUtils::isDatabaseOnline()) {
- // Write a marker file so we know to process these files when the DB comes back up.
- if (!Storage::exists('DB_WAS_DOWN')) {
- Storage::put('DB_WAS_DOWN', '');
- }
+ SubmissionUtils::markDeferredSubmissions();
$statusarray['status'] = 'OK';
$statusarray['message'] = 'Database is unavailable.';
return self::displayXMLReturnStatus($statusarray);
@@ -193,7 +183,7 @@ private function submitProcess(): Response
// Check if CTest provided us enough info to assign a buildid.
$buildid = null;
- if (request()->has('build') && request()->has('site') && request()->has('stamp')) {
+ if (request()->filled('build') && request()->filled('site') && request()->filled('stamp')) {
$build = new Build();
$build->Name = pdo_real_escape_string(request()->query('build'));
$build->ProjectId = $this->project->Id;
@@ -201,7 +191,7 @@ private function submitProcess(): Response
$build->StartTime = gmdate(FMT_DATETIME);
$build->SubmitTime = $build->StartTime;
- if (request()->has('subproject')) {
+ if (request()->filled('subproject')) {
$build->SetSubProject(pdo_real_escape_string(request()->query('subproject')));
}
@@ -229,11 +219,7 @@ private function submitProcess(): Response
fclose($fp);
unset($fp);
- // Check for marker file to see if we need to queue deferred submissions.
- if (Storage::exists('DB_WAS_DOWN')) {
- Storage::delete('DB_WAS_DOWN');
- Artisan::call('submission:queue');
- }
+ SubmissionUtils::queueDeferredSubmissions();
$statusarray['message'] = '';
if ($responseMessage !== '') {
diff --git a/app/Utils/SubmissionUtils.php b/app/Utils/SubmissionUtils.php
index a1f6e82dab..df045397e6 100644
--- a/app/Utils/SubmissionUtils.php
+++ b/app/Utils/SubmissionUtils.php
@@ -21,8 +21,11 @@
use CDash\Database;
use CDash\Model\Build;
use CDash\Model\BuildUpdate;
+use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
+use League\Flysystem\UnableToWriteFile;
class SubmissionUtils
{
@@ -331,6 +334,50 @@ public static function hashFileHandle(mixed $filehandle, string $algo): string
return hash_final($hashContext);
}
+ /** Check whether a string is a properly formatted md5 hash */
+ public static function isValidMD5(string $md5): bool
+ {
+ return preg_match('/^[a-f0-9]{32}\z/i', $md5) === 1;
+ }
+
+ /** Check whether the md5 of an open file handle matches the expected hash, ignoring case */
+ public static function isFileMD5Correct(mixed $filehandle, string $expected_md5): bool
+ {
+ return strcasecmp(self::hashFileHandle($filehandle, 'md5'), $expected_md5) === 0;
+ }
+
+ /**
+ * Save a submission file to the inbox directory
+ *
+ * @param resource|string $contents
+ */
+ public static function storeInInbox(string $filename, mixed $contents): bool
+ {
+ try {
+ return Storage::put("inbox/{$filename}", $contents);
+ } catch (UnableToWriteFile $e) {
+ report($e);
+ return false;
+ }
+ }
+
+ /** Record that the inbox holds submissions received while the database was unavailable */
+ public static function markDeferredSubmissions(): void
+ {
+ if (!Storage::exists('DB_WAS_DOWN')) {
+ Storage::put('DB_WAS_DOWN', '');
+ }
+ }
+
+ /** Queue any submissions received while the database was unavailable */
+ public static function queueDeferredSubmissions(): void
+ {
+ if (Storage::exists('DB_WAS_DOWN')) {
+ Storage::delete('DB_WAS_DOWN');
+ Artisan::call('submission:queue');
+ }
+ }
+
/** Generate the inbox filename for a CTest XML submission */
public static function xmlFilename(string $projectname, string $token_hash, string $md5): string
{
diff --git a/app/Utils/UnparsedSubmissionProcessor.php b/app/Utils/UnparsedSubmissionProcessor.php
index 92d5837cb0..32f7acb76e 100644
--- a/app/Utils/UnparsedSubmissionProcessor.php
+++ b/app/Utils/UnparsedSubmissionProcessor.php
@@ -21,14 +21,15 @@
use App\Jobs\ProcessSubmission;
use App\Models\BuildFile;
use App\Models\PendingSubmissions;
+use App\Models\Project as EloquentProject;
use App\Models\Site;
use App\Rules\ProjectNameRule;
use App\Services\AuthTokenService;
use CDash\Model\Build;
use CDash\Model\Project;
+use Closure;
use Exception;
use Illuminate\Http\JsonResponse;
-use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
@@ -98,10 +99,7 @@ public function postSubmit(): JsonResponse
$uuid = Str::uuid()->toString();
$this->serializeBuildMetadata($uuid);
- // Write a marker file so we know to process these files when the DB comes back up.
- if (!Storage::exists('DB_WAS_DOWN')) {
- Storage::put('DB_WAS_DOWN', '');
- }
+ SubmissionUtils::markDeferredSubmissions();
// Respond with success even though the database is down so that CTest will
// proceed to upload the data file.
@@ -145,17 +143,15 @@ public function parseBuildMetadata(): void
public function initializeBuild(): JsonResponse
{
// Retrieve this project from the database.
- $project_row = DB::table('project')
- ->where('name', $this->projectname)
- ->first();
+ $eloquent_project = EloquentProject::firstWhere('name', $this->projectname);
- if (!$project_row) {
+ if ($eloquent_project === null) {
abort(Response::HTTP_NOT_FOUND, 'Project does not exist');
}
- $projectid = $project_row->id;
+ $projectid = $eloquent_project->id;
// Check if this submission requires a valid authentication token.
- if (($this->token || $project_row->authenticatesubmissions) && !AuthTokenService::check($this->token, $projectid)) {
+ if (($this->token || $eloquent_project->authenticatesubmissions) && !AuthTokenService::check($this->token, $projectid)) {
abort(Response::HTTP_FORBIDDEN, 'Forbidden');
}
@@ -168,7 +164,7 @@ public function initializeBuild(): JsonResponse
// Add the build.
$this->build = new Build();
- $this->build->ProjectId = get_project_id($this->projectname);
+ $this->build->ProjectId = $projectid;
$this->build->StartTime = gmdate(FMT_DATETIME, $this->starttime);
$this->build->EndTime = gmdate(FMT_DATETIME, $this->endtime);
$this->build->SubmitTime = gmdate(FMT_DATETIME);
@@ -244,8 +240,6 @@ public function putSubmitFile(): JsonResponse
abort(Response::HTTP_BAD_REQUEST, $validator->errors()->first());
}
$this->projectname = $this->project->Name;
-
- $this->inboxdatafilename = 'inbox/' . SubmissionUtils::dataFilename($this->projectname, $this->token, $this->type, $this->buildid, $this->md5, $ext);
} else {
// Get project name from build metadata file on disk.
$projectname = null;
@@ -264,18 +258,19 @@ public function putSubmitFile(): JsonResponse
abort(Response::HTTP_NOT_FOUND, 'Build not found');
}
$this->projectname = $projectname;
- $this->inboxdatafilename = 'inbox/' . SubmissionUtils::dataFilename($this->projectname, $this->token, $this->type, $this->buildid, $this->md5, $ext);
- $this->serializeDataFileParameters();
+ }
- if (!Storage::exists('DB_WAS_DOWN')) {
- Storage::put('DB_WAS_DOWN', '');
- }
+ $filename = SubmissionUtils::dataFilename($this->projectname, $this->token, $this->type, $this->buildid, $this->md5, $ext);
+ $this->inboxdatafilename = 'inbox/' . $filename;
+
+ if (!$db_up) {
+ $this->serializeDataFileParameters();
+ SubmissionUtils::markDeferredSubmissions();
}
// Write this file to the inbox directory.
- $handle = request()->getContent(true);
- if (!Storage::put($this->inboxdatafilename, $handle)) {
- abort(Response::HTTP_INTERNAL_SERVER_ERROR, "Cannot open file ($this->inboxdatafilename)");
+ if (!SubmissionUtils::storeInInbox($filename, request()->getContent(true))) {
+ abort(Response::HTTP_INTERNAL_SERVER_ERROR, 'Failed to save submission file.');
}
if (!$db_up) {
@@ -287,14 +282,9 @@ public function putSubmitFile(): JsonResponse
// THis function will throw an exception if invalid data provided
$this->populateBuildFileRow();
- $filename = str_replace('inbox/', '', $this->inboxdatafilename);
ProcessSubmission::dispatch($filename, $this->project->Id, $this->build->Id, $this->md5);
- // Check for marker file to see if we need to queue deferred submissions.
- if (Storage::exists('DB_WAS_DOWN')) {
- Storage::delete('DB_WAS_DOWN');
- Artisan::call('submission:queue');
- }
+ SubmissionUtils::queueDeferredSubmissions();
return response()->json($response_array);
}
@@ -325,8 +315,9 @@ public function populateBuildFileRow(): void
}
// Check that the md5sum of the file matches what we were expecting.
- $md5sum = SubmissionUtils::hashFileHandle(Storage::readStream($this->inboxdatafilename), 'md5');
- if ($md5sum != $this->md5) {
+ $stream = Storage::readStream($this->inboxdatafilename);
+ if (!SubmissionUtils::isFileMD5Correct($stream, $this->md5)) {
+ $md5sum = SubmissionUtils::hashFileHandle($stream, 'md5');
Storage::delete($this->inboxdatafilename);
$buildfile->delete();
abort(Response::HTTP_BAD_REQUEST, "md5 mismatch. expected: {$this->md5}, received: $md5sum");
@@ -350,7 +341,11 @@ public function parseDataFileParameters(): void
$validator = Validator::make(request()->query(), [
'buildid' => 'required',
'type' => ['required', Rule::enum(UnparsedSubmissionType::class)],
- 'md5' => ['required', 'regex:/^[a-f0-9]{32}$/i'],
+ 'md5' => ['required', function (string $attribute, mixed $value, Closure $fail): void {
+ if (!is_string($value) || !SubmissionUtils::isValidMD5($value)) {
+ $fail('The :attribute format is invalid.');
+ }
+ }],
'filename' => 'required',
]);
@@ -455,11 +450,6 @@ private function getAuthTokenHash(): void
if ($this->token) {
return;
}
- $token = request()->bearerToken();
- if ($token) {
- $this->token = AuthTokenService::hash($token);
- } else {
- $this->token = '';
- }
+ $this->token = AuthTokenService::hash(request()->bearerToken());
}
}
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index d465221ea3..013aa68d1f 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -1404,42 +1404,6 @@ parameters:
count: 3
path: app/Http/Controllers/SubmissionController.php
- -
- rawMessage: 'Only booleans are allowed in a negated boolean, int|false given.'
- identifier: booleanNot.exprNotBoolean
- count: 1
- path: app/Http/Controllers/SubmissionController.php
-
- -
- rawMessage: 'Parameter #1 $string of function strlen expects string, (array|string) given.'
- identifier: argument.type
- count: 1
- path: app/Http/Controllers/SubmissionController.php
-
- -
- rawMessage: 'Parameter #2 $subject of function preg_match expects string, (array|string) given.'
- identifier: argument.type
- count: 1
- path: app/Http/Controllers/SubmissionController.php
-
- -
- rawMessage: 'Parameter #3 $md5 of static method App\Utils\SubmissionUtils::xmlFilename() expects string, (array|string) given.'
- identifier: argument.type
- count: 1
- path: app/Http/Controllers/SubmissionController.php
-
- -
- rawMessage: 'Part $expected_md5 (array|non-empty-string) of encapsed string cannot be cast to string.'
- identifier: encapsedStringPart.nonString
- count: 2
- path: app/Http/Controllers/SubmissionController.php
-
- -
- rawMessage: 'Part $expected_md5 (array|string) of encapsed string cannot be cast to string.'
- identifier: encapsedStringPart.nonString
- count: 2
- path: app/Http/Controllers/SubmissionController.php
-
-
rawMessage: '''
Call to deprecated method executePrepared() of class CDash\Database:
@@ -6030,12 +5994,6 @@ parameters:
count: 2
path: app/Utils/UnparsedSubmissionProcessor.php
- -
- rawMessage: 'Loose comparison via "!=" between string and mixed is not allowed.'
- identifier: notEqual.notAllowed
- count: 1
- path: app/Utils/UnparsedSubmissionProcessor.php
-
-
rawMessage: 'Method App\Utils\UnparsedSubmissionProcessor::deserializeBuildMetadata() has parameter $fp with no type specified.'
identifier: missingType.parameter
@@ -6054,18 +6012,6 @@ parameters:
count: 1
path: app/Utils/UnparsedSubmissionProcessor.php
- -
- rawMessage: 'Only booleans are allowed in a negated boolean, stdClass|null given.'
- identifier: booleanNot.exprNotBoolean
- count: 1
- path: app/Utils/UnparsedSubmissionProcessor.php
-
- -
- rawMessage: 'Only booleans are allowed in an if condition, string|null given.'
- identifier: if.condNotBoolean
- count: 1
- path: app/Utils/UnparsedSubmissionProcessor.php
-
-
rawMessage: 'Parameter #1 $json of function json_decode expects string, (string|false) given.'
identifier: argument.type
diff --git a/tests/Feature/Submission/UnparsedSubmissionTest.php b/tests/Feature/Submission/UnparsedSubmissionTest.php
index 3ece4f4d42..7c5535fd38 100644
--- a/tests/Feature/Submission/UnparsedSubmissionTest.php
+++ b/tests/Feature/Submission/UnparsedSubmissionTest.php
@@ -564,6 +564,38 @@ public function testAcceptsDataFile(): void
&& $job->expected_md5 === $md5);
}
+ public function testAcceptsDataFileWithUppercaseMd5(): void
+ {
+ $buildid = $this->createBuild();
+ $md5 = strtoupper(md5(self::FILE_CONTENTS));
+
+ $response = $this->putDataFile([
+ ...$this->dataFileParameters($buildid),
+ 'md5' => $md5,
+ ]);
+
+ $response->assertOk();
+ $response->assertExactJson(['status' => 0]);
+ $filename = "{$this->project->name}_-__-_BazelJSON_-_{$buildid}_-_{$md5}_-_.json";
+ $this->assertSame(["inbox/$filename"], Storage::allFiles());
+ $this->assertSame(self::FILE_CONTENTS, Storage::get("inbox/$filename"));
+ $this->assertDatabaseHas('buildfile', [
+ 'buildid' => $buildid,
+ 'type' => 'BazelJSON',
+ 'md5' => $md5,
+ 'filename' => 'bazel.json',
+ ]);
+ $this->assertDatabaseHas('pending_submissions', [
+ 'buildid' => $buildid,
+ 'numfiles' => 1,
+ ]);
+ Queue::assertCount(1);
+ Queue::assertPushed(ProcessSubmission::class, fn (ProcessSubmission $job) => $job->filename === $filename
+ && $job->projectid === $this->project->id
+ && $job->buildid === $buildid
+ && $job->expected_md5 === $md5);
+ }
+
public function testRejectsDataFileWithoutTokenWhenProjectRequiresAuthentication(): void
{
$this->requireAuthenticatedSubmissions();
diff --git a/tests/Feature/Submission/XmlSubmissionTest.php b/tests/Feature/Submission/XmlSubmissionTest.php
index 19d9d7ed0a..28a3ba0365 100644
--- a/tests/Feature/Submission/XmlSubmissionTest.php
+++ b/tests/Feature/Submission/XmlSubmissionTest.php
@@ -191,6 +191,29 @@ public function testAcceptsSubmissionWithMd5(): void
&& $job->expected_md5 === $md5);
}
+ public function testAcceptsSubmissionWithUppercaseMd5(): void
+ {
+ $file_to_submit = base_path('tests/Feature/Submission/data/configure.xml');
+ $md5 = md5_file($file_to_submit);
+ $this->assertIsString($md5);
+ $md5 = strtoupper($md5);
+
+ $response = $this->submit($file_to_submit, [
+ 'project' => $this->project->name,
+ 'MD5' => $md5,
+ ]);
+
+ $response->assertOk();
+ $this->assertXmlResponse($response, '
+
+ OK
+ ');
+ Queue::assertCount(1);
+ Queue::assertPushed(ProcessSubmission::class, fn (ProcessSubmission $job) => $job->projectid === $this->project->id
+ && $job->buildid === null
+ && $job->expected_md5 === $md5);
+ }
+
public function testAcceptsSubmissionWithBuildMetadata(): void
{
$response = $this->submit(base_path('tests/Feature/Submission/data/configure.xml'), [
@@ -232,6 +255,21 @@ public static function partialBuildMetadataCases(): array
'build' => 'regular_submission',
'site' => 'submission.site',
]],
+ 'with blank build' => [[
+ 'build' => '',
+ 'site' => 'submission.site',
+ 'stamp' => '20240501-0100-Nightly',
+ ]],
+ 'with blank site' => [[
+ 'build' => 'regular_submission',
+ 'site' => '',
+ 'stamp' => '20240501-0100-Nightly',
+ ]],
+ 'with blank stamp' => [[
+ 'build' => 'regular_submission',
+ 'site' => 'submission.site',
+ 'stamp' => '',
+ ]],
];
}
diff --git a/tests/Unit/Utils/SubmissionUtilsTest.php b/tests/Unit/Utils/SubmissionUtilsTest.php
index 5ef1abc4bc..63a7316590 100644
--- a/tests/Unit/Utils/SubmissionUtilsTest.php
+++ b/tests/Unit/Utils/SubmissionUtilsTest.php
@@ -3,6 +3,10 @@
namespace Tests\Unit\Utils;
use App\Utils\SubmissionUtils;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\Exceptions;
+use Illuminate\Support\Facades\Storage;
+use League\Flysystem\UnableToWriteFile;
use PHPUnit\Framework\Attributes\DataProvider;
use Tests\TestCase;
@@ -93,4 +97,131 @@ public function testParseFilenameRejectsMalformedFilename(string $filename): voi
{
$this->assertNull(SubmissionUtils::parseFilename($filename));
}
+
+ /**
+ * @return array
+ */
+ public static function wellFormedMd5s(): array
+ {
+ return [
+ 'lowercase' => [self::MD5],
+ 'uppercase' => [strtoupper(self::MD5)],
+ ];
+ }
+
+ #[DataProvider('wellFormedMd5s')]
+ public function testIsValidMD5AcceptsWellFormedHash(string $md5): void
+ {
+ $this->assertTrue(SubmissionUtils::isValidMD5($md5));
+ }
+
+ /**
+ * @return array
+ */
+ public static function malformedMd5s(): array
+ {
+ return [
+ 'empty' => [''],
+ 'too short' => [substr(self::MD5, 1)],
+ 'too long' => [self::MD5 . 'f'],
+ 'not hexadecimal' => ['z' . substr(self::MD5, 1)],
+ 'path' => ['../' . substr(self::MD5, 3)],
+ 'trailing newline' => [self::MD5 . "\n"],
+ ];
+ }
+
+ #[DataProvider('malformedMd5s')]
+ public function testIsValidMD5RejectsMalformedHash(string $md5): void
+ {
+ $this->assertFalse(SubmissionUtils::isValidMD5($md5));
+ }
+
+ public function testIsFileMD5CorrectAcceptsMatchingHash(): void
+ {
+ $stream = $this->makeStream('contents');
+
+ $this->assertTrue(SubmissionUtils::isFileMD5Correct($stream, md5('contents')));
+ $this->assertSame('contents', stream_get_contents($stream));
+ }
+
+ public function testIsFileMD5CorrectAcceptsUppercaseMatchingHash(): void
+ {
+ $stream = $this->makeStream('contents');
+
+ $this->assertTrue(SubmissionUtils::isFileMD5Correct($stream, strtoupper(md5('contents'))));
+ }
+
+ public function testIsFileMD5CorrectRejectsMismatchedHash(): void
+ {
+ $stream = $this->makeStream('contents');
+
+ $this->assertFalse(SubmissionUtils::isFileMD5Correct($stream, self::MD5));
+ }
+
+ public function testStoreInInbox(): void
+ {
+ Storage::fake();
+
+ $this->assertTrue(SubmissionUtils::storeInInbox('file.xml', $this->makeStream('contents')));
+
+ $this->assertSame(['inbox/file.xml'], Storage::allFiles());
+ $this->assertSame('contents', Storage::get('inbox/file.xml'));
+ }
+
+ public function testStoreInInboxReturnsFalseWhenWriteFails(): void
+ {
+ Exceptions::fake();
+ Storage::shouldReceive('put')
+ ->once()
+ ->with('inbox/file.xml', 'contents')
+ ->andThrow(UnableToWriteFile::atLocation('inbox/file.xml'));
+
+ $this->assertFalse(SubmissionUtils::storeInInbox('file.xml', 'contents'));
+
+ Exceptions::assertReported(UnableToWriteFile::class);
+ }
+
+ public function testMarkDeferredSubmissions(): void
+ {
+ Storage::fake();
+
+ SubmissionUtils::markDeferredSubmissions();
+
+ $this->assertSame(['DB_WAS_DOWN'], Storage::allFiles());
+ }
+
+ public function testQueueDeferredSubmissions(): void
+ {
+ Storage::fake();
+ Storage::put('DB_WAS_DOWN', '');
+ $artisan = Artisan::spy();
+
+ SubmissionUtils::queueDeferredSubmissions();
+
+ $this->assertSame([], Storage::allFiles());
+ $artisan->shouldHaveReceived('call')->once()->with('submission:queue');
+ }
+
+ public function testQueueDeferredSubmissionsDoesNothingWithoutMarker(): void
+ {
+ Storage::fake();
+ $artisan = Artisan::spy();
+
+ SubmissionUtils::queueDeferredSubmissions();
+
+ $this->assertSame([], Storage::allFiles());
+ $artisan->shouldNotHaveReceived('call');
+ }
+
+ /**
+ * @return resource
+ */
+ private function makeStream(string $contents)
+ {
+ $stream = fopen('php://memory', 'r+');
+ $this->assertIsResource($stream);
+ fwrite($stream, $contents);
+ rewind($stream);
+ return $stream;
+ }
}