Make Telegram connect codes stateless: drop the model/table, use a signed code + session
This commit is contained in:
parent
816d367dbe
commit
2284596f9d
9 changed files with 134 additions and 208 deletions
|
|
@ -4,25 +4,19 @@
|
|||
|
||||
namespace App\Enums\SocialAccount;
|
||||
|
||||
use App\Models\TelegramConnectRequest;
|
||||
use App\Models\SocialAccount;
|
||||
|
||||
enum TelegramConnectStatus: string
|
||||
{
|
||||
case Unknown = 'unknown';
|
||||
case Pending = 'pending';
|
||||
case Connected = 'connected';
|
||||
case Expired = 'expired';
|
||||
|
||||
/**
|
||||
* Derive the connection status the frontend polls for from a connect request.
|
||||
* Connected once the channel has been linked to an account; otherwise still pending.
|
||||
*/
|
||||
public static function for(?TelegramConnectRequest $request): self
|
||||
public static function for(?SocialAccount $account): self
|
||||
{
|
||||
return match (true) {
|
||||
$request === null => self::Unknown,
|
||||
$request->social_account_id !== null => self::Connected,
|
||||
$request->isExpired() => self::Expired,
|
||||
default => self::Pending,
|
||||
};
|
||||
return $account === null ? self::Pending : self::Connected;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,20 +6,21 @@
|
|||
|
||||
use App\Enums\SocialAccount\Platform as SocialPlatform;
|
||||
use App\Enums\SocialAccount\TelegramConnectStatus;
|
||||
use App\Http\Requests\App\Auth\TelegramStatusRequest;
|
||||
use App\Models\TelegramConnectRequest;
|
||||
use App\Services\Social\TelegramConnectCode;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response as SymfonyResponse;
|
||||
|
||||
class TelegramController extends SocialController
|
||||
{
|
||||
protected SocialPlatform $platform = SocialPlatform::Telegram;
|
||||
|
||||
private const SESSION_KEY = 'telegram_connect_code';
|
||||
|
||||
/**
|
||||
* Start a connection: issue a one-time code the user posts in their channel
|
||||
* (`/connect <code>`) so the webhook can tie the channel to this workspace.
|
||||
* Start a connection: issue a signed one-off code the user posts in their
|
||||
* channel (`/connect <code>`). The code carries the workspace, so the webhook
|
||||
* can link the channel without any persisted state.
|
||||
*/
|
||||
public function connect(Request $request): JsonResponse
|
||||
{
|
||||
|
|
@ -31,35 +32,39 @@ public function connect(Request $request): JsonResponse
|
|||
$this->authorize('manageAccounts', $workspace);
|
||||
$this->ensureSocialAccountLimit($workspace);
|
||||
|
||||
$connectRequest = TelegramConnectRequest::create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $request->user()->id,
|
||||
'code' => Str::lower(Str::random(12)),
|
||||
'expires_at' => now()->addMinutes(15),
|
||||
]);
|
||||
$expiresAt = now()->addMinutes(15);
|
||||
$code = TelegramConnectCode::issue($workspace->id, $expiresAt);
|
||||
|
||||
$request->session()->put(self::SESSION_KEY, $code);
|
||||
|
||||
return response()->json([
|
||||
'code' => $connectRequest->code,
|
||||
'code' => $code,
|
||||
'bot_username' => config('trypost.platforms.telegram.bot_username'),
|
||||
'expires_at' => $connectRequest->expires_at->toIso8601String(),
|
||||
'expires_at' => $expiresAt->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll whether the channel has been linked yet.
|
||||
* Poll whether the channel issued in this session has been linked yet.
|
||||
*/
|
||||
public function status(TelegramStatusRequest $request): JsonResponse
|
||||
public function status(Request $request): JsonResponse
|
||||
{
|
||||
$workspace = $request->user()->currentWorkspace;
|
||||
abort_if($workspace === null, SymfonyResponse::HTTP_CONFLICT, 'No active workspace.');
|
||||
|
||||
$connectRequest = TelegramConnectRequest::query()
|
||||
->where('workspace_id', $workspace->id)
|
||||
->where('code', $request->validated('code'))
|
||||
$payload = TelegramConnectCode::decode($request->session()->get(self::SESSION_KEY));
|
||||
|
||||
if ($payload === null) {
|
||||
return response()->json(['status' => TelegramConnectStatus::Unknown->value]);
|
||||
}
|
||||
|
||||
$account = $workspace->socialAccounts()
|
||||
->where('platform', SocialPlatform::Telegram->value)
|
||||
->where('meta->connect_nonce', data_get($payload, 'nonce'))
|
||||
->first();
|
||||
|
||||
return response()->json([
|
||||
'status' => TelegramConnectStatus::for($connectRequest)->value,
|
||||
'status' => TelegramConnectStatus::for($account)->value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
use App\Enums\SocialAccount\Status;
|
||||
use App\Features\SocialAccountLimit;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\TelegramConnectRequest;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Social\TelegramConnectCode;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
|
@ -19,8 +19,8 @@ class TelegramWebhookController extends Controller
|
|||
{
|
||||
/**
|
||||
* Receives Bot API updates. The only update we act on is a `/connect <code>`
|
||||
* message/channel_post: it ties the originating channel to the workspace that
|
||||
* generated the code. Everything else is acknowledged and ignored.
|
||||
* message/channel_post: the signed code carries the workspace, so we link the
|
||||
* originating channel to it. Everything else is acknowledged and ignored.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
|
|
@ -39,21 +39,16 @@ public function handle(Request $request): Response
|
|||
return response()->noContent();
|
||||
}
|
||||
|
||||
$connectRequest = TelegramConnectRequest::query()
|
||||
->whereNull('social_account_id')
|
||||
->where('code', $matches[1])
|
||||
->where('expires_at', '>', now())
|
||||
->first();
|
||||
$payload = TelegramConnectCode::decode($matches[1]);
|
||||
$workspace = $payload === null ? null : Workspace::find(data_get($payload, 'workspace_id'));
|
||||
|
||||
if ($connectRequest === null) {
|
||||
if ($workspace === null) {
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
$chatId = (string) data_get($chat, 'id');
|
||||
$username = data_get($chat, 'username');
|
||||
|
||||
$workspace = $connectRequest->workspace;
|
||||
|
||||
// Mirror the controller's limit gate: block only brand-new accounts, never reconnects.
|
||||
$isNewAccount = ! $workspace->socialAccounts()
|
||||
->where('platform', SocialPlatform::Telegram->value)
|
||||
|
|
@ -64,7 +59,7 @@ public function handle(Request $request): Response
|
|||
return response()->noContent();
|
||||
}
|
||||
|
||||
$account = $workspace->socialAccounts()->updateOrCreate(
|
||||
$workspace->socialAccounts()->updateOrCreate(
|
||||
[
|
||||
'platform' => SocialPlatform::Telegram->value,
|
||||
'platform_user_id' => $chatId,
|
||||
|
|
@ -83,12 +78,11 @@ public function handle(Request $request): Response
|
|||
'chat_id' => $chatId,
|
||||
'username' => $username,
|
||||
'type' => data_get($chat, 'type'),
|
||||
'connect_nonce' => data_get($payload, 'nonce'),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
$connectRequest->update(['social_account_id' => $account->id]);
|
||||
|
||||
return response()->noContent();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\App\Auth;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class TelegramStatusRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'code' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class TelegramConnectRequest extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use HasUuids;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at->isPast();
|
||||
}
|
||||
|
||||
public function workspace(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Workspace::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
53
app/Services/Social/TelegramConnectCode.php
Normal file
53
app/Services/Social/TelegramConnectCode.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Social;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* A stateless, signed `/connect` code. It carries the workspace it belongs to,
|
||||
* a one-off nonce (so the polling UI can recognise its own connection) and an
|
||||
* expiry — encrypted with the app key, so the webhook can trust it without any
|
||||
* database lookup.
|
||||
*/
|
||||
class TelegramConnectCode
|
||||
{
|
||||
public static function issue(string $workspaceId, CarbonInterface $expiresAt): string
|
||||
{
|
||||
return Crypt::encryptString((string) json_encode([
|
||||
'workspace_id' => $workspaceId,
|
||||
'nonce' => Str::lower(Str::random(16)),
|
||||
'expires_at' => $expiresAt->getTimestamp(),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode and validate a code, returning its payload or null when the code is
|
||||
* missing, tampered with, or expired.
|
||||
*
|
||||
* @return array{workspace_id: string, nonce: string, expires_at: int}|null
|
||||
*/
|
||||
public static function decode(?string $code): ?array
|
||||
{
|
||||
if (! is_string($code) || $code === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = json_decode(Crypt::decryptString($code), true);
|
||||
} catch (DecryptException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! is_array($payload) || now()->getTimestamp() > (int) data_get($payload, 'expires_at')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('telegram_connect_requests', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->foreignUuid('workspace_id')->constrained('workspaces')->cascadeOnDelete();
|
||||
$table->foreignUuid('user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('code')->unique();
|
||||
// Set by the webhook once the channel is linked; null while pending.
|
||||
$table->foreignUuid('social_account_id')->nullable()->constrained('social_accounts')->nullOnDelete();
|
||||
$table->timestamp('expires_at');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('telegram_connect_requests');
|
||||
}
|
||||
};
|
||||
|
|
@ -23,7 +23,7 @@ import {
|
|||
const open = defineModel<boolean>('open', { required: true });
|
||||
|
||||
type Phase = 'loading' | 'ready' | 'connected' | 'expired' | 'error';
|
||||
type ConnectStatus = 'unknown' | 'pending' | 'connected' | 'expired';
|
||||
type ConnectStatus = 'unknown' | 'pending' | 'connected';
|
||||
|
||||
interface ConnectResponse {
|
||||
code: string;
|
||||
|
|
@ -57,9 +57,7 @@ const poll = async () => {
|
|||
if (phase.value !== 'ready') return;
|
||||
|
||||
try {
|
||||
const response = await httpStatus.get(
|
||||
telegramStatus.url({ query: { code: code.value } }),
|
||||
);
|
||||
const response = await httpStatus.get(telegramStatus.url());
|
||||
|
||||
if (response?.status === 'connected') {
|
||||
phase.value = 'connected';
|
||||
|
|
@ -72,7 +70,8 @@ const poll = async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (response?.status === 'expired' || response?.status === 'unknown') {
|
||||
// The signed code expired (or the session was lost): prompt a fresh one.
|
||||
if (response?.status === 'unknown') {
|
||||
phase.value = 'expired';
|
||||
stopPolling();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@
|
|||
use App\Enums\SocialAccount\Platform;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Models\SocialAccount;
|
||||
use App\Models\TelegramConnectRequest;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Social\ConnectionVerifier;
|
||||
use App\Services\Social\TelegramConnectCode;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
beforeEach(function () {
|
||||
|
|
@ -43,31 +43,23 @@ function telegramUpdate(string $code, array $chat = []): array
|
|||
];
|
||||
}
|
||||
|
||||
it('issues a connect code', function () {
|
||||
it('issues a signed connect code and stores it in the session', function () {
|
||||
$response = $this->actingAs($this->user)
|
||||
->postJson(route('app.social.telegram.connect'))
|
||||
->assertOk()
|
||||
->assertJsonStructure(['code', 'bot_username', 'expires_at']);
|
||||
->assertJsonStructure(['code', 'bot_username', 'expires_at'])
|
||||
->assertSessionHas('telegram_connect_code');
|
||||
|
||||
expect($response->json('bot_username'))->toBe('TryPostBot');
|
||||
|
||||
$this->assertDatabaseHas('telegram_connect_requests', [
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'code' => $response->json('code'),
|
||||
'social_account_id' => null,
|
||||
]);
|
||||
expect(data_get(TelegramConnectCode::decode($response->json('code')), 'workspace_id'))
|
||||
->toBe($this->workspace->id);
|
||||
});
|
||||
|
||||
it('links the channel when the webhook receives a matching /connect', function () {
|
||||
$request = TelegramConnectRequest::create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'code' => 'abc123code',
|
||||
'expires_at' => now()->addMinutes(15),
|
||||
]);
|
||||
$code = TelegramConnectCode::issue($this->workspace->id, now()->addMinutes(15));
|
||||
|
||||
$this->withHeader('X-Telegram-Bot-Api-Secret-Token', 'shh-secret')
|
||||
->postJson(route('telegram.webhook'), telegramUpdate('abc123code'))
|
||||
->postJson(route('telegram.webhook'), telegramUpdate($code))
|
||||
->assertNoContent();
|
||||
|
||||
$account = SocialAccount::where('workspace_id', $this->workspace->id)
|
||||
|
|
@ -79,20 +71,15 @@ function telegramUpdate(string $code, array $chat = []): array
|
|||
expect($account->display_name)->toBe('My Channel');
|
||||
expect($account->username)->toBe('mychannel');
|
||||
expect(data_get($account->meta, 'chat_id'))->toBe('-1001234567890');
|
||||
|
||||
expect($request->fresh()->social_account_id)->toBe($account->id);
|
||||
expect(data_get($account->meta, 'connect_nonce'))
|
||||
->toBe(data_get(TelegramConnectCode::decode($code), 'nonce'));
|
||||
});
|
||||
|
||||
it('links a private channel that has no username', function () {
|
||||
TelegramConnectRequest::create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'code' => 'privatecode',
|
||||
'expires_at' => now()->addMinutes(15),
|
||||
]);
|
||||
$code = TelegramConnectCode::issue($this->workspace->id, now()->addMinutes(15));
|
||||
|
||||
$this->withHeader('X-Telegram-Bot-Api-Secret-Token', 'shh-secret')
|
||||
->postJson(route('telegram.webhook'), telegramUpdate('privatecode', ['username' => null]))
|
||||
->postJson(route('telegram.webhook'), telegramUpdate($code, ['username' => null]))
|
||||
->assertNoContent();
|
||||
|
||||
$account = SocialAccount::where('platform', Platform::Telegram)->first();
|
||||
|
|
@ -107,15 +94,10 @@ function telegramUpdate(string $code, array $chat = []): array
|
|||
|
||||
SocialAccount::factory()->count(5)->create(['workspace_id' => $this->workspace->id]);
|
||||
|
||||
TelegramConnectRequest::create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'code' => 'limitcode',
|
||||
'expires_at' => now()->addMinutes(15),
|
||||
]);
|
||||
$code = TelegramConnectCode::issue($this->workspace->id, now()->addMinutes(15));
|
||||
|
||||
$this->withHeader('X-Telegram-Bot-Api-Secret-Token', 'shh-secret')
|
||||
->postJson(route('telegram.webhook'), telegramUpdate('limitcode'))
|
||||
->postJson(route('telegram.webhook'), telegramUpdate($code))
|
||||
->assertNoContent();
|
||||
|
||||
expect($this->workspace->socialAccounts()->count())->toBe(5);
|
||||
|
|
@ -133,15 +115,10 @@ function telegramUpdate(string $code, array $chat = []): array
|
|||
'platform_user_id' => '-1001234567890',
|
||||
]);
|
||||
|
||||
TelegramConnectRequest::create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'code' => 'reconnectcode',
|
||||
'expires_at' => now()->addMinutes(15),
|
||||
]);
|
||||
$code = TelegramConnectCode::issue($this->workspace->id, now()->addMinutes(15));
|
||||
|
||||
$this->withHeader('X-Telegram-Bot-Api-Secret-Token', 'shh-secret')
|
||||
->postJson(route('telegram.webhook'), telegramUpdate('reconnectcode'))
|
||||
->postJson(route('telegram.webhook'), telegramUpdate($code))
|
||||
->assertNoContent();
|
||||
|
||||
expect($this->workspace->socialAccounts()->count())->toBe(5);
|
||||
|
|
@ -150,58 +127,56 @@ function telegramUpdate(string $code, array $chat = []): array
|
|||
)->toBe(1);
|
||||
});
|
||||
|
||||
it('requires a code to check connection status', function () {
|
||||
$this->actingAs($this->user)
|
||||
->getJson(route('app.social.telegram.status'))
|
||||
->assertStatus(422);
|
||||
});
|
||||
|
||||
it('rejects the webhook without the secret token', function () {
|
||||
$this->postJson(route('telegram.webhook'), telegramUpdate('whatever'))
|
||||
$code = TelegramConnectCode::issue($this->workspace->id, now()->addMinutes(15));
|
||||
|
||||
$this->postJson(route('telegram.webhook'), telegramUpdate($code))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('ignores the webhook for an unknown or expired code', function () {
|
||||
TelegramConnectRequest::create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'code' => 'expiredcode',
|
||||
'expires_at' => now()->subMinute(),
|
||||
]);
|
||||
it('ignores the webhook for a tampered or expired code', function () {
|
||||
$expired = TelegramConnectCode::issue($this->workspace->id, now()->subMinute());
|
||||
|
||||
$this->withHeader('X-Telegram-Bot-Api-Secret-Token', 'shh-secret')
|
||||
->postJson(route('telegram.webhook'), telegramUpdate('expiredcode'))
|
||||
->postJson(route('telegram.webhook'), telegramUpdate($expired))
|
||||
->assertNoContent();
|
||||
|
||||
$this->withHeader('X-Telegram-Bot-Api-Secret-Token', 'shh-secret')
|
||||
->postJson(route('telegram.webhook'), telegramUpdate('does-not-exist'))
|
||||
->postJson(route('telegram.webhook'), telegramUpdate('not-a-valid-code'))
|
||||
->assertNoContent();
|
||||
|
||||
expect(SocialAccount::where('platform', Platform::Telegram)->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('reports connection status while pending and once connected', function () {
|
||||
$request = TelegramConnectRequest::create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'code' => 'statuscode',
|
||||
'expires_at' => now()->addMinutes(15),
|
||||
]);
|
||||
it('reports the session connection status while pending and once connected', function () {
|
||||
$code = TelegramConnectCode::issue($this->workspace->id, now()->addMinutes(15));
|
||||
$nonce = data_get(TelegramConnectCode::decode($code), 'nonce');
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->getJson(route('app.social.telegram.status', ['code' => 'statuscode']))
|
||||
->withSession(['telegram_connect_code' => $code])
|
||||
->getJson(route('app.social.telegram.status'))
|
||||
->assertOk()
|
||||
->assertJson(['status' => 'pending']);
|
||||
|
||||
$account = SocialAccount::factory()->telegram()->create(['workspace_id' => $this->workspace->id]);
|
||||
$request->update(['social_account_id' => $account->id]);
|
||||
SocialAccount::factory()->telegram()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'meta' => ['chat_id' => '-1001234567890', 'username' => 'mychannel', 'type' => 'channel', 'connect_nonce' => $nonce],
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->getJson(route('app.social.telegram.status', ['code' => 'statuscode']))
|
||||
->withSession(['telegram_connect_code' => $code])
|
||||
->getJson(route('app.social.telegram.status'))
|
||||
->assertOk()
|
||||
->assertJson(['status' => 'connected']);
|
||||
});
|
||||
|
||||
it('reports unknown status without a session code', function () {
|
||||
$this->actingAs($this->user)
|
||||
->getJson(route('app.social.telegram.status'))
|
||||
->assertOk()
|
||||
->assertJson(['status' => 'unknown']);
|
||||
});
|
||||
|
||||
it('verifies a connected telegram account via getChat', function () {
|
||||
config(['trypost.platforms.telegram.bot_token' => 'TESTTOKEN']);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue