feat: implement save mechanisim
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
@@ -182,11 +182,92 @@ class CompositionManager {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function save(string $tenantId, string $userId, string $identifier, array $data): array {
|
||||||
|
// Construct the snapshot mutation that will run while the composition is locked.
|
||||||
|
$saveComposition = function (?array $composed) use ($data): ?array {
|
||||||
|
if ($composed === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($data['revision']) && is_int($data['revision'])) {
|
||||||
|
$composed['revision'] = $data['revision'];
|
||||||
|
}
|
||||||
|
$composed['sender'] = $data['sender'];
|
||||||
|
$composed['message'] = $data['message'];
|
||||||
|
return $this->markRemoteDirty($composed);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the mutation and atomically persist the snapshot it returns.
|
||||||
|
$composed = $this->compositionStore->compositionSave(
|
||||||
|
$tenantId,
|
||||||
|
$userId,
|
||||||
|
$identifier,
|
||||||
|
$saveComposition,
|
||||||
|
);
|
||||||
|
if ($composed === null) {
|
||||||
|
return [
|
||||||
|
'identifier' => $identifier,
|
||||||
|
'disposition' => 'error',
|
||||||
|
'error' => [
|
||||||
|
'type' => 'composition_not_found',
|
||||||
|
'message' => 'Composition not found',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$synchronized = $this->synchronize($tenantId, $userId, $identifier);
|
||||||
|
if ($synchronized['disposition'] !== 'saved') {
|
||||||
|
return [
|
||||||
|
'identifier' => $identifier,
|
||||||
|
'disposition' => 'error',
|
||||||
|
'error' => [
|
||||||
|
'type' => 'composition_save_failed',
|
||||||
|
'message' => (string)($synchronized['error'] ?? 'Draft could not be saved to the server'),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->compositionStore->compositionDiscard($tenantId, $userId, $identifier);
|
||||||
|
return ['identifier' => $identifier, 'disposition' => 'saved'];
|
||||||
|
}
|
||||||
|
|
||||||
public function discard(string $tenantId, string $userId, string $identifier): array {
|
public function discard(string $tenantId, string $userId, string $identifier): array {
|
||||||
return [
|
$composed = $this->compositionStore->compositionFetch($tenantId, $userId, $identifier);
|
||||||
'identifier' => $identifier,
|
if ($composed === null) {
|
||||||
'disposition' => $this->compositionStore->compositionDiscard($tenantId, $userId, $identifier),
|
return ['identifier' => $identifier, 'disposition' => 'discarded'];
|
||||||
];
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$remoteEntity = $this->remoteEntityIdentifier($composed['remote']['entity'] ?? null);
|
||||||
|
if ($remoteEntity !== null) {
|
||||||
|
$outcomes = $this->mailManager->entityDelete($tenantId, $userId, $remoteEntity);
|
||||||
|
$outcome = $outcomes[(string)$remoteEntity] ?? reset($outcomes);
|
||||||
|
if (!is_array($outcome) || ($outcome['disposition'] ?? 'error') === 'error') {
|
||||||
|
return [
|
||||||
|
'identifier' => $identifier,
|
||||||
|
'disposition' => 'error',
|
||||||
|
'error' => [
|
||||||
|
'type' => 'composition_discard_failed',
|
||||||
|
'message' => is_array($outcome)
|
||||||
|
? (string)($outcome['error'] ?? 'Remote draft could not be discarded')
|
||||||
|
: 'Remote draft could not be discarded',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable $throwable) {
|
||||||
|
return [
|
||||||
|
'identifier' => $identifier,
|
||||||
|
'disposition' => 'error',
|
||||||
|
'error' => [
|
||||||
|
'type' => 'composition_discard_failed',
|
||||||
|
'message' => $throwable->getMessage(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->compositionStore->compositionDiscard($tenantId, $userId, $identifier);
|
||||||
|
return ['identifier' => $identifier, 'disposition' => 'discarded'];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function synchronize(string $tenantId, string $userId, string $identifier): array {
|
public function synchronize(string $tenantId, string $userId, string $identifier): array {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ final class CompositionController extends ControllerAbstract {
|
|||||||
$result = match ($operation) {
|
$result = match ($operation) {
|
||||||
'stage' => $this->stage($tenantId, $userId, $data),
|
'stage' => $this->stage($tenantId, $userId, $data),
|
||||||
'patch' => $this->patch($tenantId, $userId, $data),
|
'patch' => $this->patch($tenantId, $userId, $data),
|
||||||
|
'save' => $this->save($tenantId, $userId, $data),
|
||||||
'discard' => $this->discard($tenantId, $userId, $data),
|
'discard' => $this->discard($tenantId, $userId, $data),
|
||||||
'send' => $this->send($tenantId, $userId, $data),
|
'send' => $this->send($tenantId, $userId, $data),
|
||||||
'attachment.add' => $this->attachmentAdd($tenantId, $userId, $data),
|
'attachment.add' => $this->attachmentAdd($tenantId, $userId, $data),
|
||||||
@@ -108,6 +109,21 @@ final class CompositionController extends ControllerAbstract {
|
|||||||
|
|
||||||
return $this->compositionManager->patch($tenantId, $userId, $data['identifier'], $data);
|
return $this->compositionManager->patch($tenantId, $userId, $data['identifier'], $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function save(string $tenantId, string $userId, array $data): array {
|
||||||
|
if (!isset($data['identifier']) || !is_string($data['identifier']) || $data['identifier'] === '') {
|
||||||
|
throw new InvalidArgumentException('Invalid parameter: identifier must be a non-empty string');
|
||||||
|
}
|
||||||
|
if (!isset($data['sender']) || !is_array($data['sender'])) {
|
||||||
|
throw new InvalidArgumentException('Invalid parameter: sender must be an array');
|
||||||
|
}
|
||||||
|
if (!isset($data['message']) || !is_array($data['message'])) {
|
||||||
|
throw new InvalidArgumentException('Invalid parameter: message must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->compositionManager->save($tenantId, $userId, $data['identifier'], $data);
|
||||||
|
}
|
||||||
|
|
||||||
private function discard(string $tenantId, string $userId, array $data): array {
|
private function discard(string $tenantId, string $userId, array $data): array {
|
||||||
if (!isset($data['identifier']) || !is_string($data['identifier']) || $data['identifier'] === '') {
|
if (!isset($data['identifier']) || !is_string($data['identifier']) || $data['identifier'] === '') {
|
||||||
throw new InvalidArgumentException('Invalid parameter: identifier must be a non-empty string');
|
throw new InvalidArgumentException('Invalid parameter: identifier must be a non-empty string');
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ const showCc = ref(false)
|
|||||||
const showBcc = ref(false)
|
const showBcc = ref(false)
|
||||||
const applyingDraftToEditor = ref(false)
|
const applyingDraftToEditor = ref(false)
|
||||||
const showDocumentSelector = ref(false)
|
const showDocumentSelector = ref(false)
|
||||||
|
const showCloseDialog = ref(false)
|
||||||
|
const closeAction = ref<'save' | 'discard' | null>(null)
|
||||||
const documentSelector = computed(() => documentsSelectorComponent())
|
const documentSelector = computed(() => documentsSelectorComponent())
|
||||||
|
|
||||||
const sender = computed({
|
const sender = computed({
|
||||||
@@ -160,9 +162,32 @@ onBeforeUnmount(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const handleClose = async () => {
|
const handleClose = () => {
|
||||||
await compositionStore.closeDraft()
|
showCloseDialog.value = true
|
||||||
emit('close')
|
}
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
closeAction.value = 'save'
|
||||||
|
try {
|
||||||
|
if (await compositionStore.saveDraft()) {
|
||||||
|
showCloseDialog.value = false
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
closeAction.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDiscard = async () => {
|
||||||
|
closeAction.value = 'discard'
|
||||||
|
try {
|
||||||
|
if (await compositionStore.discardDraft()) {
|
||||||
|
showCloseDialog.value = false
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
closeAction.value = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
@@ -233,7 +258,9 @@ const isActive = (name: string, attrs?: any) => {
|
|||||||
:status="stageStatus"
|
:status="stageStatus"
|
||||||
:can-send="canSend"
|
:can-send="canSend"
|
||||||
:sending="sending"
|
:sending="sending"
|
||||||
|
:saving="saving"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
|
@save="handleSave"
|
||||||
@send="handleSend"
|
@send="handleSend"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -294,6 +321,38 @@ const isActive = (name: string, attrs?: any) => {
|
|||||||
:selectable="['entity']"
|
:selectable="['entity']"
|
||||||
@select="handleDocumentsSelected"
|
@select="handleDocumentsSelected"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-dialog v-model="showCloseDialog" max-width="480" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Save this draft?</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
Save the draft to your mail server, or discard it permanently.
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn :disabled="closeAction !== null" @click="showCloseDialog = false">
|
||||||
|
Cancel
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="text"
|
||||||
|
:loading="closeAction === 'discard'"
|
||||||
|
:disabled="closeAction !== null || saving"
|
||||||
|
@click="handleDiscard"
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
:loading="closeAction === 'save'"
|
||||||
|
:disabled="closeAction !== null || saving"
|
||||||
|
@click="handleSave"
|
||||||
|
>
|
||||||
|
Save and close
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ interface Props {
|
|||||||
status: string
|
status: string
|
||||||
canSend: boolean
|
canSend: boolean
|
||||||
sending: boolean
|
sending: boolean
|
||||||
|
saving: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>()
|
defineProps<Props>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
close: []
|
close: []
|
||||||
|
save: []
|
||||||
send: []
|
send: []
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
@@ -37,9 +39,20 @@ defineEmits<{
|
|||||||
{{ status }}
|
{{ status }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
class="mr-2"
|
||||||
|
variant="text"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="sending"
|
||||||
|
prepend-icon="mdi-content-save-outline"
|
||||||
|
@click="$emit('save')"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
color="primary"
|
color="primary"
|
||||||
:disabled="!canSend"
|
:disabled="!canSend || saving"
|
||||||
:loading="sending"
|
:loading="sending"
|
||||||
prepend-icon="mdi-send"
|
prepend-icon="mdi-send"
|
||||||
@click="$emit('send')"
|
@click="$emit('send')"
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import type {
|
|||||||
CompositionDiscardResponse,
|
CompositionDiscardResponse,
|
||||||
CompositionPatchRequest,
|
CompositionPatchRequest,
|
||||||
CompositionPatchResponse,
|
CompositionPatchResponse,
|
||||||
|
CompositionSaveRequest,
|
||||||
|
CompositionSaveResponse,
|
||||||
CompositionSendRequest,
|
CompositionSendRequest,
|
||||||
CompositionSendResponse,
|
CompositionSendResponse,
|
||||||
CompositionStageRequest,
|
CompositionStageRequest,
|
||||||
@@ -50,6 +52,10 @@ const compositionService = {
|
|||||||
return await post<CompositionPatchRequest, CompositionPatchResponse>('patch', request)
|
return await post<CompositionPatchRequest, CompositionPatchResponse>('patch', request)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async save(request: CompositionSaveRequest): Promise<CompositionSaveResponse> {
|
||||||
|
return await post<CompositionSaveRequest, CompositionSaveResponse>('save', request)
|
||||||
|
},
|
||||||
|
|
||||||
async discard(request: CompositionDiscardRequest): Promise<CompositionDiscardResponse> {
|
async discard(request: CompositionDiscardRequest): Promise<CompositionDiscardResponse> {
|
||||||
return await post<CompositionDiscardRequest, CompositionDiscardResponse>('discard', request)
|
return await post<CompositionDiscardRequest, CompositionDiscardResponse>('discard', request)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
CompositionAttachmentInterface,
|
CompositionAttachmentInterface,
|
||||||
CompositionAttachmentRemoveRequest,
|
CompositionAttachmentRemoveRequest,
|
||||||
CompositionPatchRequest,
|
CompositionPatchRequest,
|
||||||
|
CompositionSaveRequest,
|
||||||
CompositionStageRequest,
|
CompositionStageRequest,
|
||||||
CompositionStageResponse,
|
CompositionStageResponse,
|
||||||
} from '@/types/composition'
|
} from '@/types/composition'
|
||||||
@@ -123,10 +124,6 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
|||||||
stageDraft()
|
stageDraft()
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDraft() {
|
|
||||||
return discardDraft()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function stageDraft() {
|
async function stageDraft() {
|
||||||
if (!activeDraft.value) {
|
if (!activeDraft.value) {
|
||||||
return null
|
return null
|
||||||
@@ -189,16 +186,73 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
|||||||
|
|
||||||
async function discardDraft() {
|
async function discardDraft() {
|
||||||
if (!activeDraft.value) {
|
if (!activeDraft.value) {
|
||||||
return
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoSaveTimer) {
|
||||||
|
clearTimeout(autoSaveTimer)
|
||||||
|
autoSaveTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const draftId = activeDraft.value.identifier
|
const draftId = activeDraft.value.identifier
|
||||||
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
await compositionService.discard({ identifier: draftId })
|
const response = await compositionService.discard({ identifier: draftId })
|
||||||
|
if (response.disposition === 'error') {
|
||||||
|
mailUiStore.notify(`Failed to discard draft: ${response.error?.message || 'Unknown error'}`, 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
activeDraft.value = null
|
||||||
|
return true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Mail][Composer] Failed to discard staged draft:', error)
|
console.error('[Mail][Composer] Failed to discard staged draft:', error)
|
||||||
|
mailUiStore.notify('Failed to discard draft', 'error')
|
||||||
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveDraft() {
|
||||||
|
if (!activeDraft.value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoSaveTimer) {
|
||||||
|
clearTimeout(autoSaveTimer)
|
||||||
|
autoSaveTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const draft = activeDraft.value
|
||||||
|
const request: CompositionSaveRequest = {
|
||||||
|
identifier: draft.identifier,
|
||||||
|
revision: draft.revision,
|
||||||
|
sender: {
|
||||||
|
provider: draft.sender.service.provider,
|
||||||
|
service: draft.sender.service.identifier,
|
||||||
|
address: draft.sender.address,
|
||||||
|
name: draft.sender.label || null,
|
||||||
|
},
|
||||||
|
message: draft.message,
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const response = await compositionService.save(request)
|
||||||
|
if (response.disposition === 'error') {
|
||||||
|
mailUiStore.notify(`Failed to save draft: ${response.error?.message || 'Unknown error'}`, 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
activeDraft.value = null
|
activeDraft.value = null
|
||||||
|
mailUiStore.notify('Draft saved', 'success')
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mail][Composer] Failed to save draft:', error)
|
||||||
|
mailUiStore.notify('Failed to save draft', 'error')
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +533,8 @@ export const useMailCompositionStore = defineStore('mailCompositionStore', () =>
|
|||||||
stageStatus,
|
stageStatus,
|
||||||
senderIdentities,
|
senderIdentities,
|
||||||
openDraft,
|
openDraft,
|
||||||
closeDraft,
|
saveDraft,
|
||||||
|
discardDraft,
|
||||||
sendDraft,
|
sendDraft,
|
||||||
updateSender,
|
updateSender,
|
||||||
updateRecipients,
|
updateRecipients,
|
||||||
|
|||||||
@@ -58,6 +58,17 @@ export interface CompositionPatchResponse {
|
|||||||
message: CompositionMessageInterface
|
message: CompositionMessageInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CompositionSaveRequest extends CompositionPatchRequest {}
|
||||||
|
|
||||||
|
export interface CompositionSaveResponse {
|
||||||
|
identifier: string
|
||||||
|
disposition: 'saved' | 'error'
|
||||||
|
error?: {
|
||||||
|
type: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface CompositionDiscardRequest {
|
export interface CompositionDiscardRequest {
|
||||||
identifier: string
|
identifier: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,109 @@ final class CompositionSynchronizationTest extends TestCase {
|
|||||||
$this->assertNull($snapshot['remote']['entity']);
|
$this->assertNull($snapshot['remote']['entity']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testInlineSaveSynchronizesAndPurgesLocalComposition(): void {
|
||||||
|
$this->stageComposition();
|
||||||
|
$properties = $this->createStub(MessagePropertiesMutableInterface::class);
|
||||||
|
$service = $this->mutableService(['draftTarget' => 'drafts'], $properties);
|
||||||
|
$created = $this->entity('provider', 'service', 'drafts', 'created-inline');
|
||||||
|
|
||||||
|
$mailManager = $this->createMock(\KTXM\MailManager\Manager::class);
|
||||||
|
$mailManager->method('serviceFindByAddress')->willReturn($service);
|
||||||
|
$mailManager->expects($this->once())
|
||||||
|
->method('entityCreate')
|
||||||
|
->willReturn($created);
|
||||||
|
|
||||||
|
$result = $this->compositionManager($mailManager)->save(
|
||||||
|
self::TENANT_ID,
|
||||||
|
self::USER_ID,
|
||||||
|
self::COMPOSITION_ID,
|
||||||
|
[
|
||||||
|
'revision' => 2,
|
||||||
|
'sender' => ['address' => 'sender@example.com'],
|
||||||
|
'message' => ['subject' => 'Saved inline'],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame('saved', $result['disposition']);
|
||||||
|
$this->assertNull($this->compositionStore->compositionFetch(
|
||||||
|
self::TENANT_ID,
|
||||||
|
self::USER_ID,
|
||||||
|
self::COMPOSITION_ID,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInlineSaveFailureKeepsLocalComposition(): void {
|
||||||
|
$this->stageComposition();
|
||||||
|
$mailManager = $this->createStub(\KTXM\MailManager\Manager::class);
|
||||||
|
$mailManager->method('serviceFindByAddress')->willReturn(null);
|
||||||
|
|
||||||
|
$result = $this->compositionManager($mailManager)->save(
|
||||||
|
self::TENANT_ID,
|
||||||
|
self::USER_ID,
|
||||||
|
self::COMPOSITION_ID,
|
||||||
|
[
|
||||||
|
'revision' => 2,
|
||||||
|
'sender' => ['address' => 'sender@example.com'],
|
||||||
|
'message' => ['subject' => 'Keep this draft'],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$snapshot = $this->compositionStore->compositionFetch(self::TENANT_ID, self::USER_ID, self::COMPOSITION_ID);
|
||||||
|
$this->assertSame('error', $result['disposition']);
|
||||||
|
$this->assertSame('composition_save_failed', $result['error']['type']);
|
||||||
|
$this->assertSame('Keep this draft', $snapshot['message']['subject']);
|
||||||
|
$this->assertSame('failed', $snapshot['remote']['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDiscardDeletesRemoteDraftBeforePurgingLocalComposition(): void {
|
||||||
|
$remote = 'provider:service:drafts:remote-1';
|
||||||
|
$this->stageComposition($remote);
|
||||||
|
$mailManager = $this->createMock(\KTXM\MailManager\Manager::class);
|
||||||
|
$mailManager->expects($this->once())
|
||||||
|
->method('entityDelete')
|
||||||
|
->with(
|
||||||
|
self::TENANT_ID,
|
||||||
|
self::USER_ID,
|
||||||
|
$this->callback(static fn(EntityIdentifier $target): bool => (string)$target === $remote),
|
||||||
|
)
|
||||||
|
->willReturn([$remote => ['disposition' => 'deleted']]);
|
||||||
|
|
||||||
|
$result = $this->compositionManager($mailManager)->discard(
|
||||||
|
self::TENANT_ID,
|
||||||
|
self::USER_ID,
|
||||||
|
self::COMPOSITION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame('discarded', $result['disposition']);
|
||||||
|
$this->assertNull($this->compositionStore->compositionFetch(
|
||||||
|
self::TENANT_ID,
|
||||||
|
self::USER_ID,
|
||||||
|
self::COMPOSITION_ID,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRemoteDiscardFailureKeepsLocalComposition(): void {
|
||||||
|
$remote = 'provider:service:drafts:remote-1';
|
||||||
|
$this->stageComposition($remote);
|
||||||
|
$mailManager = $this->createMock(\KTXM\MailManager\Manager::class);
|
||||||
|
$mailManager->method('entityDelete')
|
||||||
|
->willReturn([$remote => ['disposition' => 'error', 'error' => 'Provider unavailable']]);
|
||||||
|
|
||||||
|
$result = $this->compositionManager($mailManager)->discard(
|
||||||
|
self::TENANT_ID,
|
||||||
|
self::USER_ID,
|
||||||
|
self::COMPOSITION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertSame('error', $result['disposition']);
|
||||||
|
$this->assertSame('Provider unavailable', $result['error']['message']);
|
||||||
|
$this->assertNotNull($this->compositionStore->compositionFetch(
|
||||||
|
self::TENANT_ID,
|
||||||
|
self::USER_ID,
|
||||||
|
self::COMPOSITION_ID,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
private function stageComposition(?string $remoteEntity = null): void {
|
private function stageComposition(?string $remoteEntity = null): void {
|
||||||
$saveComposition = static fn(): array => [
|
$saveComposition = static fn(): array => [
|
||||||
'identifier' => self::COMPOSITION_ID,
|
'identifier' => self::COMPOSITION_ID,
|
||||||
|
|||||||
Reference in New Issue
Block a user