set('dewemoji.data_path', base_path('tests/Fixtures/emojis.fixture.json')); } public function test_categories_endpoint_returns_category_map(): void { $response = $this->getJson('/v1/categories'); $response ->assertOk() ->assertHeader('X-Dewemoji-Tier', 'free') ->assertJsonPath('Smileys & Emotion.0', 'face-smiling') ->assertJsonPath('People & Body.0', 'hand-fingers-closed'); } public function test_emojis_endpoint_supports_both_q_and_query_params(): void { $byQ = $this->getJson('/v1/emojis?q=grinning'); $byQuery = $this->getJson('/v1/emojis?query=grinning'); $byQ->assertOk()->assertJsonPath('total', 1); $byQuery->assertOk()->assertJsonPath('total', 1); $byQ->assertJsonPath('items.0.slug', 'grinning-face'); $byQuery->assertJsonPath('items.0.slug', 'grinning-face'); } public function test_license_verify_returns_ok_when_accept_all_is_enabled(): void { config()->set('dewemoji.license.accept_all', true); config()->set('dewemoji.billing.mode', 'live'); $response = $this->postJson('/v1/license/verify', [ 'key' => 'dummy-key', 'account_id' => 'acct_123', 'version' => '1.0.0', ]); $response ->assertOk() ->assertHeader('X-Dewemoji-Tier', 'pro') ->assertJson([ 'ok' => true, 'tier' => 'pro', ]); } public function test_license_verify_returns_401_when_live_and_key_is_not_valid(): void { config()->set('dewemoji.billing.mode', 'live'); config()->set('dewemoji.license.accept_all', false); config()->set('dewemoji.license.pro_keys', []); config()->set('dewemoji.billing.providers.gumroad.enabled', false); config()->set('dewemoji.billing.providers.mayar.enabled', false); $response = $this->postJson('/v1/license/verify', [ 'key' => 'not-valid', ]); $response ->assertStatus(401) ->assertHeader('X-Dewemoji-Tier', 'free') ->assertJsonPath('ok', false) ->assertJsonPath('error', 'invalid_license') ->assertJsonPath('details.gumroad', 'gumroad_disabled') ->assertJsonPath('details.mayar', 'mayar_disabled'); } public function test_license_verify_uses_gumroad_stub_key_when_enabled(): void { config()->set('dewemoji.billing.mode', 'live'); config()->set('dewemoji.license.accept_all', false); config()->set('dewemoji.license.pro_keys', []); config()->set('dewemoji.billing.providers.gumroad.enabled', true); config()->set('dewemoji.billing.providers.gumroad.test_keys', ['gumroad-dev-key']); config()->set('dewemoji.billing.providers.mayar.enabled', false); $response = $this->postJson('/v1/license/verify', [ 'key' => 'gumroad-dev-key', ]); $response ->assertOk() ->assertJsonPath('ok', true) ->assertJsonPath('source', 'gumroad') ->assertJsonPath('plan', 'pro'); } public function test_license_verify_uses_gumroad_live_payload_mapping(): void { config()->set('dewemoji.billing.mode', 'live'); config()->set('dewemoji.license.accept_all', false); config()->set('dewemoji.license.pro_keys', []); config()->set('dewemoji.billing.providers.gumroad.enabled', true); config()->set('dewemoji.billing.providers.gumroad.verify_url', 'https://api.gumroad.com/v2/licenses/verify'); config()->set('dewemoji.billing.providers.gumroad.product_ids', ['prod_123']); config()->set('dewemoji.billing.providers.mayar.enabled', false); Http::fake([ 'https://api.gumroad.com/*' => Http::response([ 'success' => true, 'purchase' => [ 'product_id' => 'prod_123', 'recurrence' => 'monthly', ], ], 200), ]); $response = $this->postJson('/v1/license/verify', [ 'key' => 'gum-live-key', ]); $response ->assertOk() ->assertJsonPath('ok', true) ->assertJsonPath('source', 'gumroad') ->assertJsonPath('product_id', 'prod_123'); } public function test_license_verify_uses_mayar_live_payload_mapping(): void { config()->set('dewemoji.billing.mode', 'live'); config()->set('dewemoji.license.accept_all', false); config()->set('dewemoji.license.pro_keys', []); config()->set('dewemoji.billing.providers.gumroad.enabled', false); config()->set('dewemoji.billing.providers.mayar.enabled', true); config()->set('dewemoji.billing.providers.mayar.verify_url', 'https://api.mayar.id/v1/license/verify'); config()->set('dewemoji.billing.providers.mayar.api_key', 'secret'); Http::fake([ 'https://api.mayar.id/*' => Http::response([ 'success' => true, 'data' => [ 'valid' => true, 'product_id' => 'mayar_prod_1', 'type' => 'lifetime', 'expires_at' => null, ], ], 200), ]); $response = $this->postJson('/v1/license/verify', [ 'key' => 'mayar-live-key', ]); $response ->assertOk() ->assertJsonPath('ok', true) ->assertJsonPath('source', 'mayar') ->assertJsonPath('product_id', 'mayar_prod_1'); } public function test_emoji_detail_by_slug_endpoint_returns_item(): void { config()->set('dewemoji.billing.mode', 'sandbox'); $response = $this->getJson('/v1/emoji/grinning-face'); $response ->assertOk() ->assertJsonPath('slug', 'grinning-face') ->assertJsonPath('name', 'grinning face'); } public function test_emoji_detail_by_query_slug_endpoint_returns_item(): void { $response = $this->getJson('/v1/emoji?slug=thumbs-up'); $response ->assertOk() ->assertJsonPath('slug', 'thumbs-up') ->assertJsonPath('supports_skin_tone', true); } public function test_license_activate_and_deactivate_in_sandbox_mode(): void { config()->set('dewemoji.billing.mode', 'sandbox'); config()->set('dewemoji.license.max_devices', 3); $activate = $this->postJson('/v1/license/activate', [ 'key' => 'any-test-key', 'email' => 'dev@dewemoji.test', 'product' => 'chrome', 'device_id' => 'device-1', ]); $activate ->assertOk() ->assertJsonPath('ok', true) ->assertJsonPath('pro', true) ->assertJsonPath('product', 'chrome') ->assertJsonPath('device_id', 'device-1'); $deactivate = $this->postJson('/v1/license/deactivate', [ 'key' => 'any-test-key', 'product' => 'chrome', 'device_id' => 'device-1', ]); $deactivate ->assertOk() ->assertJsonPath('ok', true) ->assertJsonPath('product', 'chrome') ->assertJsonPath('device_id', 'device-1'); } public function test_free_daily_limit_returns_429_after_cap(): void { config()->set('dewemoji.billing.mode', 'live'); config()->set('dewemoji.license.accept_all', false); config()->set('dewemoji.license.pro_keys', []); config()->set('dewemoji.free_daily_limit', 1); $first = $this->getJson('/v1/emojis?q=happy&page=1&limit=10'); $first->assertOk()->assertJsonPath('usage.used', 1); $second = $this->getJson('/v1/emojis?q=good&page=1&limit=10'); $second ->assertStatus(429) ->assertJsonPath('error', 'daily_limit_reached'); } public function test_emojis_endpoint_returns_pro_tier_when_live_key_is_whitelisted(): void { config()->set('dewemoji.billing.mode', 'live'); config()->set('dewemoji.license.accept_all', false); config()->set('dewemoji.license.pro_keys', ['pro-key-123']); config()->set('dewemoji.billing.providers.gumroad.enabled', false); config()->set('dewemoji.billing.providers.mayar.enabled', false); $response = $this->getJson('/v1/emojis?q=grinning&key=pro-key-123'); $response ->assertOk() ->assertHeader('X-Dewemoji-Tier', 'pro') ->assertJsonPath('total', 1); } public function test_emojis_endpoint_accepts_authorization_bearer_key(): void { config()->set('dewemoji.billing.mode', 'live'); config()->set('dewemoji.license.accept_all', false); config()->set('dewemoji.license.pro_keys', ['bearer-pro-key']); $response = $this ->withHeaders(['Authorization' => 'Bearer bearer-pro-key']) ->getJson('/v1/emojis?q=grinning'); $response ->assertOk() ->assertHeader('X-Dewemoji-Tier', 'pro'); } public function test_emojis_endpoint_accepts_x_license_key_header(): void { config()->set('dewemoji.billing.mode', 'live'); config()->set('dewemoji.license.accept_all', false); config()->set('dewemoji.license.pro_keys', ['header-pro-key']); $response = $this ->withHeaders(['X-License-Key' => 'header-pro-key']) ->getJson('/v1/emojis?q=grinning'); $response ->assertOk() ->assertHeader('X-Dewemoji-Tier', 'pro'); } public function test_metrics_endpoint_requires_token_or_allowed_ip(): void { config()->set('dewemoji.metrics.enabled', true); config()->set('dewemoji.metrics.token', 'secret-token'); config()->set('dewemoji.metrics.allow_ips', []); $forbidden = $this->getJson('/v1/metrics'); $forbidden->assertStatus(403); $allowed = $this->getJson('/v1/metrics?token=secret-token'); $allowed->assertOk()->assertJsonPath('ok', true); } }