diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..5710634 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Stream Chat PHP SDK - Environment Variables Example +# Copy this file to .env or .env.test and fill in your values + +# API Credentials +# STREAM_KEY=your_api_key +# STREAM_SECRET=your_api_secret + +# Test Configuration +# STREAM_CHAT_TIMEOUT=3.0 + +# STREAM_HOST is the base URL for the Stream Chat API +# STREAM_HOST=http://localhost:3030 # Use localhost to access the local machine +# STREAM_HOST=http://host.docker.internal:3030 # Use host.docker.internal to access the host machine from Docker +# STREAM_HOST=https://chat.stream-io-api.com #[default] # Use the production API \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5428792 --- /dev/null +++ b/Makefile @@ -0,0 +1,167 @@ +SHELL := bash +.ONESHELL: +.SHELLFLAGS := -eu -o pipefail -c +.DELETE_ON_ERROR: +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules + +# Docker configuration +DOCKER_IMAGE := stream-chat-php +DOCKER_TAG := latest +DOCKER_CONTAINER := stream-chat-php-test +DOCKER_RUN := docker run --rm -v $(PWD):/app -w /app --add-host=host.docker.internal:host-gateway + +# Environment variables +ENV_FILE ?= .env + +# PHP configuration +PHP_VERSION ?= 8.2 +COMPOSER_FLAGS ?= --prefer-dist --no-interaction + +# Test configuration +PHPUNIT := vendor/bin/phpunit +PHPUNIT_FLAGS ?= --colors=always +PHPUNIT_FILTER ?= + +# Find all .env files in the env directory (if it exists) +ENV_DIR := env +ENV_FILES := $(wildcard $(ENV_DIR)/*.env) +TEST_TARGETS := $(addprefix docker-test-, $(subst .env,,$(subst $(ENV_DIR)/,,$(ENV_FILES)))) + +# Default target +.PHONY: help +help: + @echo "Stream Chat PHP SDK Makefile" + @echo "" + @echo "Usage:" + @echo " make docker-build Build the Docker image" + @echo " make docker-test Run all tests in Docker" + @echo " make docker-test-unit Run unit tests in Docker" + @echo " make docker-test-integration Run integration tests in Docker" + @echo " make docker-lint Run PHP CS Fixer in Docker" + @echo " make docker-lint-fix Fix code style issues in Docker" + @echo " make docker-analyze Run static analysis (Phan) in Docker" + @echo " make docker-shell Start a shell in the Docker container" + @echo " make docker-clean Remove Docker container and image" + @echo " make docker-install Install dependencies in Docker" + @echo " make docker-update Update dependencies in Docker" + @echo "" + @echo "Environment:" + @echo " ENV_FILE Path to .env file (default: .env)" + @echo " PHP_VERSION PHP version to use (default: 8.2)" + @echo " PHPUNIT_FLAGS Additional flags for PHPUnit" + @echo " PHPUNIT_FILTER Filter for PHPUnit tests" + @echo "" + +# Docker image build +.PHONY: docker-build +docker-build: + @echo "Building Docker image $(DOCKER_IMAGE):$(DOCKER_TAG)..." + docker build -t $(DOCKER_IMAGE):$(DOCKER_TAG) \ + --build-arg PHP_VERSION=$(PHP_VERSION) \ + -f docker/Dockerfile . + +# Ensure the Docker image is built +.PHONY: ensure-image +ensure-image: + @if ! docker image inspect $(DOCKER_IMAGE):$(DOCKER_TAG) > /dev/null 2>&1; then \ + $(MAKE) docker-build; \ + fi + +# Run all tests in Docker +.PHONY: docker-test +docker-test: ensure-image .env + @echo "Running all tests in Docker..." + $(DOCKER_RUN) --env-file $(ENV_FILE) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + $(PHPUNIT) $(PHPUNIT_FLAGS) $(PHPUNIT_FILTER) + +# Run unit tests in Docker +.PHONY: docker-test-unit +docker-test-unit: ensure-image .env + @echo "Running unit tests in Docker..." + $(DOCKER_RUN) --env-file $(ENV_FILE) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + $(PHPUNIT) $(PHPUNIT_FLAGS) --testsuite "Unit Test Suite" $(PHPUNIT_FILTER) + +# Run integration tests in Docker +.PHONY: docker-test-integration +docker-test-integration: ensure-image .env + @echo "Running integration tests in Docker..." + $(DOCKER_RUN) --env-file $(ENV_FILE) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + $(PHPUNIT) $(PHPUNIT_FLAGS) --testsuite "Integration Test Suite" $(PHPUNIT_FILTER) + +# Run PHP CS Fixer in Docker +.PHONY: docker-lint +docker-lint: ensure-image + @echo "Running PHP CS Fixer in Docker..." + $(DOCKER_RUN) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + vendor/bin/php-cs-fixer fix --dry-run --diff + +# Fix code style issues +.PHONY: docker-lint-fix +docker-lint-fix: ensure-image + @echo "Fixing code style issues..." + $(DOCKER_RUN) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + vendor/bin/php-cs-fixer fix + +# Run static analysis +.PHONY: docker-analyze +docker-analyze: ensure-image + @echo "Running static analysis..." + $(DOCKER_RUN) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + vendor/bin/phan --color + +# Start a shell in the Docker container +.PHONY: docker-shell +docker-shell: ensure-image + @echo "Starting shell in Docker container..." + $(DOCKER_RUN) -it $(DOCKER_IMAGE):$(DOCKER_TAG) bash + +# Clean up Docker resources +.PHONY: docker-clean +docker-clean: + @echo "Cleaning up Docker resources..." + -docker rm -f $(DOCKER_CONTAINER) 2>/dev/null || true + -docker rmi -f $(DOCKER_IMAGE):$(DOCKER_TAG) 2>/dev/null || true + +# Create .env file if it doesn't exist by copying from .env.example +.env: .env.example + @echo "Creating .env file from .env.example..." + @cp .env.example .env + @echo "Created .env file. Please edit it with your configuration." + +# Create .env.test file for testing +.PHONY: .env.test +.env.test: .env.example + @echo "Creating .env.test file from .env.example..." + @cp .env.example .env.test + @echo "Created .env.test file. Please edit it with your test configuration." + +# Dynamic targets for environment-specific tests +.PHONY: $(TEST_TARGETS) +$(TEST_TARGETS): ensure-image .env + $(eval TARGET := $(subst docker-test-,,$@)) + $(eval ENV_FILE := $(ENV_DIR)/$(TARGET).env) + @echo "Running tests with environment $(TARGET)..." + $(DOCKER_RUN) --env-file $(ENV_FILE) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + $(PHPUNIT) $(PHPUNIT_FLAGS) $(PHPUNIT_FILTER) + +# Install dependencies +.PHONY: docker-install +docker-install: ensure-image + @echo "Installing dependencies..." + $(DOCKER_RUN) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + composer install $(COMPOSER_FLAGS) + +# Update dependencies +.PHONY: docker-update +docker-update: ensure-image + @echo "Updating dependencies..." + $(DOCKER_RUN) $(DOCKER_IMAGE):$(DOCKER_TAG) \ + composer update $(COMPOSER_FLAGS) + +# Run tests with test environment +.PHONY: docker-test-with-test-env +docker-test-with-test-env: ensure-image .env.test + @echo "Running all tests in Docker with test environment..." + $(DOCKER_RUN) --env-file .env.test $(DOCKER_IMAGE):$(DOCKER_TAG) \ + $(PHPUNIT) $(PHPUNIT_FLAGS) $(PHPUNIT_FILTER) \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..dcb9adc --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,41 @@ +ARG PHP_VERSION=8.2 + +FROM php:${PHP_VERSION}-cli + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + unzip \ + libzip-dev \ + && rm -rf /var/lib/apt/lists/* + +# Install PHP extensions +RUN docker-php-ext-install \ + zip \ + pcntl + +# Install AST extension for Phan +RUN pecl install ast && \ + docker-php-ext-enable ast + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Set working directory +WORKDIR /app + +# Copy composer files +COPY composer.json composer.lock* ./ + +# Install dependencies +RUN composer install --prefer-dist --no-scripts --no-progress --no-interaction + +# Copy the rest of the application +COPY . . + +# Set environment variables +ENV PHAN_ALLOW_XDEBUG=0 +ENV PHAN_DISABLE_XDEBUG_WARN=1 + +# Default command +CMD ["php", "-v"] \ No newline at end of file diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..ccf461c --- /dev/null +++ b/docker/README.md @@ -0,0 +1,103 @@ +# Docker-based Testing for Stream Chat PHP SDK + +This directory contains Docker configuration for testing the Stream Chat PHP SDK in an isolated environment. + +## Requirements + +- Docker installed on your system +- Make (usually pre-installed on macOS and Linux) + +## Quick Start + +1. Build the Docker image: + ``` + make docker-build + ``` + +2. Run all tests: + ``` + make docker-test + ``` + This will automatically create a `.env` file from `.env.example` if it doesn't exist. + +3. Run tests with test environment: + ``` + make docker-test-with-test-env + ``` + This will use `.env.test` for environment variables. + +4. Run only unit tests: + ``` + make docker-test-unit + ``` + +5. Run only integration tests: + ``` + make docker-test-integration + ``` + +## Available Commands + +All Docker-related commands are prefixed with `docker-` to distinguish them from local commands: + +- `make docker-build` - Build the Docker image +- `make docker-test` - Run all tests +- `make docker-test-with-test-env` - Run all tests with test environment +- `make docker-test-unit` - Run unit tests +- `make docker-test-integration` - Run integration tests +- `make docker-lint` - Check code style with PHP CS Fixer +- `make docker-lint-fix` - Fix code style issues +- `make docker-analyze` - Run static analysis with Phan +- `make docker-shell` - Start a shell in the Docker container +- `make docker-clean` - Remove Docker container and image +- `make docker-install` - Install dependencies +- `make docker-update` - Update dependencies + +## Environment Variables + +The project includes a `.env.example` file with commented example values. When you run tests, the Makefile will: + +1. Create a `.env` file from `.env.example` if it doesn't exist +2. Create a `.env.test` file from `.env.example` when running `make docker-test-with-test-env` + +You can also use environment-specific files in the `env/` directory: + +``` +make docker-test-[environment_name] +``` + +### Environment File Hierarchy + +- `.env` - Default environment file for general testing +- `.env.test` - Environment file specifically for test environments +- `env/*.env` - Environment files for specific test scenarios + +## Accessing Host Machine from Docker + +When running tests in Docker, the container's `localhost` refers to the container itself, not the host machine. To access services running on your host machine (like a local server on port 3030), use `host.docker.internal` instead of `localhost`. + +For example, in your `.env` file: +``` +STREAM_HOST=http://host.docker.internal:3030 +``` + +The Makefile includes the `--add-host=host.docker.internal:host-gateway` flag to ensure this works across different Docker environments. + +## Customizing PHP Version + +You can specify a different PHP version when building the Docker image: +``` +make docker-build PHP_VERSION=8.3 +``` + +## Additional PHPUnit Options + +You can pass additional options to PHPUnit: +``` +make docker-test PHPUNIT_FLAGS="--verbose --filter=testSpecificMethod" +``` + +Or use the filter directly: +``` +make docker-test PHPUNIT_FILTER="testSpecificMethod" +``` diff --git a/lib/GetStream/StreamChat/Client.php b/lib/GetStream/StreamChat/Client.php index 57433cd..420d4ef 100644 --- a/lib/GetStream/StreamChat/Client.php +++ b/lib/GetStream/StreamChat/Client.php @@ -1659,4 +1659,73 @@ public function queryDrafts(string $userId, ?array $filter = null, ?array $sort return $this->post("drafts/query", $data); } + + /** + * Creates a reminder for a message. + * + * @param string $messageId The ID of the message to create a reminder for + * @param string $userId The ID of the user creating the reminder + * @param DateTime|null $remindAt When to remind the user (optional) + * @return StreamResponse API response + * @throws StreamException + */ + public function createReminder(string $messageId, string $userId, ?DateTime $remindAt = null): StreamResponse + { + $data = ['user_id' => $userId]; + if ($remindAt instanceof DateTime) { + $data['remind_at'] = $remindAt->format(DateTime::RFC3339); + } + return $this->post("messages/{$messageId}/reminders", $data); + } + + /** + * Updates a reminder for a message. + * + * @param string $messageId The ID of the message with the reminder + * @param string $userId The ID of the user who owns the reminder + * @param DateTime|null $remindAt When to remind the user (optional) + * @return StreamResponse API response + * @throws StreamException + */ + public function updateReminder(string $messageId, string $userId, ?DateTime $remindAt = null): StreamResponse + { + $data = ['user_id' => $userId]; + if ($remindAt instanceof DateTime) { + $data['remind_at'] = $remindAt->format(DateTime::RFC3339); + } + return $this->patch("messages/{$messageId}/reminders", $data); + } + + /** + * Deletes a reminder for a message. + * + * @param string $messageId The ID of the message with the reminder + * @param string $userId The ID of the user who owns the reminder + * @return StreamResponse API response + * @throws StreamException + */ + public function deleteReminder(string $messageId, string $userId): StreamResponse + { + return $this->delete("messages/{$messageId}/reminders", ['user_id' => $userId]); + } + + /** + * Queries reminders based on filter conditions. + * + * @param string $userId The ID of the user whose reminders to query + * @param array $filterConditions Conditions to filter reminders + * @param array|null $sort Sort parameters (default: [['field' => 'remind_at', 'direction' => 1]]) + * @param array $options Additional query options like limit, offset + * @return StreamResponse API response with reminders + * @throws StreamException + */ + public function queryReminders(string $userId, array $filterConditions = [], ?array $sort = null, array $options = []): StreamResponse + { + $params = array_merge($options, [ + 'filter_conditions' => $filterConditions, + 'sort' => $sort ?? [['field' => 'remind_at', 'direction' => 1]], + 'user_id' => $userId + ]); + return $this->post('reminders/query', $params); + } } diff --git a/tests/integration/IntegrationTest.php b/tests/integration/IntegrationTest.php index e59c9ee..5301576 100644 --- a/tests/integration/IntegrationTest.php +++ b/tests/integration/IntegrationTest.php @@ -976,7 +976,9 @@ public function testChannelAddModerators() public function testChannelMarkRead() { - $response = $this->channel->markRead($this->user1["id"]); + $channelMember = $this->getUser(); + $this->channel->addMembers([$channelMember["id"]]); + $response = $this->channel->markRead($channelMember["id"]); $this->assertTrue(array_key_exists("event", (array)$response)); $this->assertSame($response["event"]["type"], "message.read"); } diff --git a/tests/integration/ReminderTest.php b/tests/integration/ReminderTest.php new file mode 100644 index 0000000..eaaad68 --- /dev/null +++ b/tests/integration/ReminderTest.php @@ -0,0 +1,155 @@ +client = new Client(getenv('STREAM_KEY'), getenv('STREAM_SECRET')); + $this->user = $this->getUser(); + $this->channel = $this->getChannel(); + $this->channel->updatePartial([ + "config_overrides" => ["user_message_reminders" => true], + ]); + + // Create a message to use for reminders + $message = [ + 'text' => 'This is a test message for reminders' + ]; + $response = $this->channel->sendMessage($message, $this->user['id']); + $this->messageId = $response['message']['id']; + } + + protected function tearDown(): void + { + try { + $this->channel->delete(); + $this->client->deleteUser($this->user['id'], ["user" => "hard", "messages" => "hard"]); + } catch (\Exception $e) { + // We don't care about cleanup errors + } + } + + private function getUser(): array + { + $userId = 'reminder-test-user-' . uniqid(); + $user = [ + 'id' => $userId, + 'name' => 'Reminder Test User', + ]; + $this->client->upsertUser($user); + return $user; + } + + public function getChannel(): \GetStream\StreamChat\Channel + { + $channelId = 'reminder-test-channel-' . uniqid(); + $channel = $this->client->Channel('messaging', $channelId); + $channel->create($this->user['id']); + return $channel; + } + + public function testCreateReminder() + { + $remindAt = new DateTime('+1 day'); + $response = $this->client->createReminder($this->messageId, $this->user['id'], $remindAt); + + $this->assertArrayHasKey('reminder', $response); + $this->assertEquals($this->messageId, $response['reminder']['message_id']); + $this->assertEquals($this->user['id'], $response['reminder']['user_id']); + $this->assertNotEmpty($response['reminder']['remind_at']); + } + + public function testCreateReminderWithoutRemindAt() + { + $response = $this->client->createReminder($this->messageId, $this->user['id']); + + $this->assertArrayHasKey('reminder', $response); + $this->assertEquals($this->messageId, $response['reminder']['message_id']); + $this->assertEquals($this->user['id'], $response['reminder']['user_id']); + } + + public function testUpdateReminder() + { + // First create a reminder + $this->client->createReminder($this->messageId, $this->user['id']); + + // Then update it + $newRemindAt = new DateTime('+2 days'); + $response = $this->client->updateReminder($this->messageId, $this->user['id'], $newRemindAt); + + $this->assertArrayHasKey('reminder', $response); + $this->assertEquals($this->messageId, $response['reminder']['message_id']); + $this->assertEquals($this->user['id'], $response['reminder']['user_id']); + $this->assertNotEmpty($response['reminder']['remind_at']); + } + + public function testDeleteReminder() + { + // First create a reminder + $this->client->createReminder($this->messageId, $this->user['id']); + + // Then delete it + $response = $this->client->deleteReminder($this->messageId, $this->user['id']); + + // The response is a StreamResponse object, so we'll just check that it exists + $this->assertNotNull($response); + $this->assertTrue(true); // If we got here, the test passed + } + + public function testQueryReminders() + { + // Create a reminder + $remindAt = new DateTime('+1 day'); + $this->client->createReminder($this->messageId, $this->user['id'], $remindAt); + + // Query reminders + $response = $this->client->queryReminders($this->user['id']); + + $this->assertArrayHasKey('reminders', $response); + $this->assertGreaterThan(0, count($response['reminders'])); + + // Test with filter conditions + $filterConditions = [ + 'message_id' => $this->messageId + ]; + $response = $this->client->queryReminders($this->user['id'], $filterConditions); + + $this->assertArrayHasKey('reminders', $response); + $this->assertGreaterThan(0, count($response['reminders'])); + $this->assertEquals($this->messageId, $response['reminders'][0]['message_id']); + } +}