비즈뿌리오 알림톡 템플릿을 콘솔 위임(발송 전 실시간 조회) 방식에서 시스템 내 작성(draft) → 검수 신청(requested) → 승인(approved) 라이프사이클로 개편한다. 발송 판정의 SSoT 를 DB(bizppurio_templates)로 옮기고 발송 전 실시간 조회를 폐지. - 알림↔템플릿 바인딩 모델(notification_bindings) 제거, 알림 1건당 템플릿 1행으로 대체 - 검수 신청: template_code 자체 채번(codeCheck 재시도) → kapi add/update → request - 상태 동기화: 30분 스케줄러 + 수동 새로고침, 승인 전이 시 content 를 approved_content 로 동결 - SMS 본문 언어별 입력 복원(수신자 로케일 발송), SMS 단독 선택 시 알림톡 미발송 - 3면(코어/게시판/이커머스) '비즈뿌리오' 통합 탭 + 플러그인 '알림 템플릿 관리' 화면 - 중복 검수 신청은 서버 원자 선점(claimForInspection 조건부 UPDATE)으로 차단 - admin_basic 1.0.6 / board 1.0.5 / ecommerce 1.1.2: 채널 서브탭 통합 탭 선언(tab_channels) 지원 - 검증: 계획서 대비 5라운드 전수검증, §6.3 실측 매트릭스 Playwright 19 고정, PHPUnit 421 / Vitest 228 green, audit 0 error
339 lines
12 KiB
PHP
339 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Plugins\Sirsoft\MessageBizppurio\Tests\Unit\Services;
|
|
|
|
use App\Services\PluginSettingsService;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Mockery;
|
|
use Plugins\Sirsoft\MessageBizppurio\Exceptions\BizppurioApiException;
|
|
use Plugins\Sirsoft\MessageBizppurio\Services\BizppurioKakaoApiClient;
|
|
use Plugins\Sirsoft\MessageBizppurio\Tests\PluginTestCase;
|
|
|
|
/**
|
|
* BizppurioKakaoApiClient — kapi 단일 도메인, bizId+apiKey body 인증 검증.
|
|
*/
|
|
class BizppurioKakaoApiClientTest extends PluginTestCase
|
|
{
|
|
private const IDENTIFIER = 'sirsoft-message_bizppurio';
|
|
|
|
/**
|
|
* @param array<string, string> $settings
|
|
*/
|
|
private function makeSettings(array $settings): PluginSettingsService
|
|
{
|
|
$mock = Mockery::mock(PluginSettingsService::class);
|
|
$mock->shouldReceive('get')->with(self::IDENTIFIER)->andReturn($settings);
|
|
|
|
return $mock;
|
|
}
|
|
|
|
private function client(): BizppurioKakaoApiClient
|
|
{
|
|
return new BizppurioKakaoApiClient(
|
|
$this->makeSettings(['bizppurio_id' => 'biz01', 'api_key' => 'key01']),
|
|
);
|
|
}
|
|
|
|
public function test_발신프로필_조회는_bizid_apikey를_body에_싣는다(): void
|
|
{
|
|
Http::fake([
|
|
'kapi.ppurio.com/*' => Http::response(['code' => '200', 'data' => ['success' => []]], 200),
|
|
]);
|
|
|
|
$result = $this->client()->getSenderProfiles();
|
|
|
|
$this->assertTrue($this->client()->isSuccess($result));
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), 'kapi.ppurio.com/v3/kakao/profile/use')
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_템플릿_목록은_senderkey를_포함한다(): void
|
|
{
|
|
Http::fake([
|
|
'kapi.ppurio.com/*' => Http::response(['code' => '200', 'data' => []], 200),
|
|
]);
|
|
|
|
$this->client()->getTemplateList('SENDER_KEY', ['count' => 20]);
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/list')
|
|
&& $request['senderKey'] === 'SENDER_KEY'
|
|
&& $request['count'] === 20
|
|
&& $request['bizId'] === 'biz01';
|
|
});
|
|
}
|
|
|
|
public function test_템플릿_상세_조회(): void
|
|
{
|
|
Http::fake([
|
|
'kapi.ppurio.com/*' => Http::response(['code' => '200', 'data' => ['templateCode' => 'TW_1']], 200),
|
|
]);
|
|
|
|
$result = $this->client()->getTemplateDetail('SK', 'TW_1');
|
|
|
|
$this->assertSame('TW_1', $result['data']['templateCode']);
|
|
}
|
|
|
|
public function test_임의_엔드포인트_request_위임(): void
|
|
{
|
|
Http::fake([
|
|
'kapi.ppurio.com/*' => Http::response(['code' => '200'], 200),
|
|
]);
|
|
|
|
$this->client()->request('/v3/kakao/template/add', ['templateName' => 'T']);
|
|
|
|
Http::assertSent(fn ($request) => str_contains($request->url(), '/v3/kakao/template/add')
|
|
&& $request['templateName'] === 'T');
|
|
}
|
|
|
|
public function test_자격증명_미설정시_예외(): void
|
|
{
|
|
$client = new BizppurioKakaoApiClient(
|
|
$this->makeSettings(['bizppurio_id' => 'biz01', 'api_key' => '']),
|
|
);
|
|
|
|
$this->expectException(BizppurioApiException::class);
|
|
$client->getSenderProfiles();
|
|
}
|
|
|
|
public function test_http_실패시_예외(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response([], 500)]);
|
|
|
|
$this->expectException(BizppurioApiException::class);
|
|
$this->client()->getSenderProfiles();
|
|
}
|
|
|
|
public function test_http_실패시_카카오_message와_code를_예외에_싣는다(): void
|
|
{
|
|
Http::fake([
|
|
'kapi.ppurio.com/*' => Http::response(
|
|
['code' => '403', 'message' => '접근할 수 없는 IP 입니다. (114.207.113.206)'],
|
|
403,
|
|
),
|
|
]);
|
|
|
|
try {
|
|
$this->client()->getSenderProfiles();
|
|
$this->fail('BizppurioApiException 이 발생해야 한다.');
|
|
} catch (BizppurioApiException $e) {
|
|
$this->assertSame('접근할 수 없는 IP 입니다. (114.207.113.206)', $e->getMessage());
|
|
$this->assertSame('403', $e->getResultCode());
|
|
$this->assertSame(403, $e->getHttpStatus());
|
|
}
|
|
}
|
|
|
|
public function test_http_실패_body에_message가_없으면_일반문구로_폴백한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response([], 500)]);
|
|
|
|
try {
|
|
$this->client()->getSenderProfiles();
|
|
$this->fail('BizppurioApiException 이 발생해야 한다.');
|
|
} catch (BizppurioApiException $e) {
|
|
$this->assertSame(
|
|
__('sirsoft-message_bizppurio::messages.error.kakao_request_failed'),
|
|
$e->getMessage(),
|
|
);
|
|
$this->assertNull($e->getResultCode());
|
|
$this->assertSame(500, $e->getHttpStatus());
|
|
}
|
|
}
|
|
|
|
public function test_템플릿_등록은_add_경로로_pos_t한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->addTemplate([
|
|
'senderKey' => 'SK',
|
|
'templateCode' => 'TW_1',
|
|
'templateName' => '주문완료',
|
|
]);
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/add')
|
|
&& $request['senderKey'] === 'SK'
|
|
&& $request['templateCode'] === 'TW_1'
|
|
&& $request['templateName'] === '주문완료'
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_템플릿_수정은_update_경로로_pos_t한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->updateTemplate([
|
|
'senderKey' => 'SK',
|
|
'templateCode' => 'TW_1',
|
|
'newTemplateCode' => 'TW_2',
|
|
]);
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/update')
|
|
&& $request['newTemplateCode'] === 'TW_2'
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_템플릿_삭제는_delete_경로로_pos_t한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->deleteTemplate('SK', 'TW_1');
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/delete')
|
|
&& $request['senderKey'] === 'SK'
|
|
&& $request['templateCode'] === 'TW_1'
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_템플릿_코드_중복검증은_code_check_경로로_pos_t한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->checkTemplateCode('SK', 'TW_1');
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/codeCheck')
|
|
&& $request['senderKey'] === 'SK'
|
|
&& $request['templateCode'] === 'TW_1'
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_검수요청은_comment를_포함해_request_경로로_pos_t한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->requestInspection('SK', 'TW_1', '변수는 주문번호입니다.');
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/request')
|
|
&& $request['senderKey'] === 'SK'
|
|
&& $request['templateCode'] === 'TW_1'
|
|
&& $request['comment'] === '변수는 주문번호입니다.'
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_검수요청_comment_미전달시_comment_키를_싣지_않는다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->requestInspection('SK', 'TW_1');
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/request')
|
|
&& $request['senderKey'] === 'SK'
|
|
&& ! array_key_exists('comment', $request->data())
|
|
&& $request['bizId'] === 'biz01';
|
|
});
|
|
}
|
|
|
|
public function test_검수취소는_cancel_request_경로로_pos_t한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->cancelInspection('SK', 'TW_1');
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/cancel_request')
|
|
&& $request['senderKey'] === 'SK'
|
|
&& $request['templateCode'] === 'TW_1'
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_승인취소는_cancel_approval_경로로_pos_t한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->cancelApproval('SK', 'TW_1');
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/cancel_approval')
|
|
&& $request['senderKey'] === 'SK'
|
|
&& $request['templateCode'] === 'TW_1'
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_휴면해제는_release_경로로_pos_t한다(): void
|
|
{
|
|
Http::fake(['kapi.ppurio.com/*' => Http::response(['code' => '200'], 200)]);
|
|
|
|
$this->client()->releaseDormant('SK', 'TW_1');
|
|
|
|
Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), '/v3/kakao/template/release')
|
|
&& $request['senderKey'] === 'SK'
|
|
&& $request['templateCode'] === 'TW_1'
|
|
&& $request['bizId'] === 'biz01'
|
|
&& $request['apiKey'] === 'key01';
|
|
});
|
|
}
|
|
|
|
public function test_이미지_업로드는_multipart로_전송하고_최상위_image_필드를_반환한다(): void
|
|
{
|
|
// kapi 유일의 비-JSON(multipart) 엔드포인트 — 응답도 data 봉투가 아니라
|
|
// 최상위 image 필드에 업로드 URL 이 실린다(부록 A-7).
|
|
Http::fake([
|
|
'kapi.ppurio.com/*' => Http::response([
|
|
'code' => '200',
|
|
'message' => 'success',
|
|
'image' => 'https://mud-kage.kakao.com/dn/example.png',
|
|
], 200),
|
|
]);
|
|
|
|
$tmpPath = tempnam(sys_get_temp_dir(), 'biz_img_');
|
|
file_put_contents($tmpPath, 'fake-png-bytes');
|
|
|
|
try {
|
|
$result = $this->client()->uploadTemplateImage($tmpPath, 'template.png');
|
|
} finally {
|
|
@unlink($tmpPath);
|
|
}
|
|
|
|
$this->assertSame('https://mud-kage.kakao.com/dn/example.png', $result['image']);
|
|
$this->assertArrayNotHasKey('data', $result);
|
|
|
|
Http::assertSent(function ($request) {
|
|
if (! str_contains($request->url(), '/v3/kakao/image/alimtalk/template')) {
|
|
return false;
|
|
}
|
|
|
|
if (! $request->isMultipart()) {
|
|
return false;
|
|
}
|
|
|
|
// multipart 파트에 image 파일 + bizId/apiKey 필드가 실려야 한다.
|
|
// 파일 contents 는 스트림 리소스로 실리므로(전송 후 close) 값 비교가 불가 —
|
|
// 파트명 + 원본 파일명으로 단언한다.
|
|
$parts = collect($request->data());
|
|
|
|
return $request->hasFile('image', null, 'template.png')
|
|
&& $parts->contains(fn ($part) => ($part['name'] ?? null) === 'bizId' && ($part['contents'] ?? null) === 'biz01')
|
|
&& $parts->contains(fn ($part) => ($part['name'] ?? null) === 'apiKey' && ($part['contents'] ?? null) === 'key01');
|
|
});
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
}
|