Initial commit: OpenClaw Mattermost Extension
This commit is contained in:
+564
@@ -0,0 +1,564 @@
|
||||
# Contributing to @lumbridgecorp/openclaw-mattermost
|
||||
|
||||
Thank you for your interest in contributing! This document provides guidelines for setting up your development environment, running tests, and submitting contributions.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Development Setup](#development-setup)
|
||||
- [Running Tests](#running-tests)
|
||||
- [Test Structure](#test-structure)
|
||||
- [Writing Tests](#writing-tests)
|
||||
- [Code Style](#code-style)
|
||||
- [Pull Request Process](#pull-request-process)
|
||||
- [Release Process](#release-process)
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js 22+** (required for native fetch and WebSocket support)
|
||||
- **pnpm** (preferred) or npm/yarn
|
||||
- **Git**
|
||||
- **Mattermost server** (for integration tests)
|
||||
|
||||
### Local Setup
|
||||
|
||||
1. **Fork the repository** on GitHub
|
||||
|
||||
2. **Clone your fork:**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/openclaw-extentions-mattermost.git
|
||||
cd openclaw-extentions-mattermost
|
||||
```
|
||||
|
||||
3. **Install dependencies:**
|
||||
```bash
|
||||
pnpm install
|
||||
# or
|
||||
npm install
|
||||
```
|
||||
|
||||
4. **Build the project:**
|
||||
```bash
|
||||
pnpm build
|
||||
# or
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. **Verify the build:**
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Quick Test Commands
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run tests in watch mode (for development)
|
||||
pnpm test -- --watch
|
||||
|
||||
# Run tests with coverage
|
||||
pnpm test -- --coverage
|
||||
|
||||
# Run a specific test file
|
||||
pnpm test src/mattermost/client.test.ts
|
||||
|
||||
# Run tests matching a pattern
|
||||
pnpm test -- -t "should create client"
|
||||
|
||||
# Run tests with verbose output
|
||||
pnpm test -- --verbose
|
||||
```
|
||||
|
||||
### Test Categories
|
||||
|
||||
#### Unit Tests
|
||||
|
||||
Fast, isolated tests that don't require external services:
|
||||
|
||||
```bash
|
||||
# Run only unit tests (no integration)
|
||||
pnpm test -- --testPathIgnorePatterns="integration|e2e"
|
||||
```
|
||||
|
||||
Unit tests cover:
|
||||
- Error handling classes
|
||||
- Configuration validation
|
||||
- Utility functions
|
||||
- Retry logic
|
||||
- Security checks
|
||||
|
||||
#### Integration Tests with Docker Compose
|
||||
|
||||
Integration tests use Docker Compose to spin up a complete Mattermost instance with PostgreSQL. This provides isolated, reproducible tests against a real Mattermost API.
|
||||
|
||||
**Setup and Run Integration Tests:**
|
||||
|
||||
```bash
|
||||
# Start the test infrastructure
|
||||
docker-compose -f tests/integration/docker-compose.test.yml up -d
|
||||
|
||||
# Wait for Mattermost to be ready (first startup may take 1-2 minutes)
|
||||
docker-compose -f tests/integration/docker-compose.test.yml ps
|
||||
|
||||
# Run integration tests
|
||||
pnpm test tests/integration/
|
||||
|
||||
# Run specific integration test file
|
||||
pnpm test tests/integration/authentication.test.ts
|
||||
|
||||
# Run integration tests with verbose output
|
||||
pnpm test tests/integration/ --verbose
|
||||
|
||||
# Clean up after tests
|
||||
docker-compose -f tests/integration/docker-compose.test.yml down -v
|
||||
```
|
||||
|
||||
**Integration Test Coverage:**
|
||||
|
||||
| Test File | Coverage |
|
||||
|-----------|----------|
|
||||
| `authentication.test.ts` | Bot token auth, PAT auth, error scenarios |
|
||||
| `send-message.test.ts` | Send to channel/DM, threads, props, error cases |
|
||||
| `edit-message.test.ts` | Edit own messages, props, permission errors |
|
||||
| `delete-message.test.ts` | Delete messages, thread handling, cleanup |
|
||||
| `download-file.test.ts` | File download, security restrictions, MIME validation |
|
||||
| `interactive-buttons.test.ts` | Button rendering, directives, end-to-end flows |
|
||||
|
||||
**Environment Variables for Integration Tests:**
|
||||
|
||||
```bash
|
||||
# Optional: Override defaults (only if not using Docker Compose)
|
||||
export MATTERMOST_URL="http://localhost:8065"
|
||||
export MATTERMOST_SYSADMIN_USER="sysadmin"
|
||||
export MATTERMOST_SYSADMIN_PASSWORD="Sys@dmin123"
|
||||
export MATTERMOST_TEST_USER="testuser"
|
||||
export MATTERMOST_TEST_USER_PASSWORD="Test@user123"
|
||||
export MATTERMOST_BOT_USERNAME="testbot"
|
||||
```
|
||||
|
||||
**Test Data Cleanup:**
|
||||
|
||||
Integration tests automatically clean up:
|
||||
- Created posts after each test suite
|
||||
- Uploaded files
|
||||
- Test channels and teams (via Docker volume reset)
|
||||
|
||||
Run `docker-compose -f tests/integration/docker-compose.test.yml down -v` to completely reset the test environment between runs.
|
||||
|
||||
**Using a Custom Mattermost Instance:**
|
||||
|
||||
If you prefer to use an existing Mattermost instance for integration tests:
|
||||
|
||||
```bash
|
||||
# Set up environment variables
|
||||
export MATTERMOST_URL="https://chat-test.example.com"
|
||||
export MATTERMOST_SYSADMIN_TOKEN="your-sysadmin-token"
|
||||
export MATTERMOST_BOT_TOKEN="your-bot-token"
|
||||
|
||||
# Run integration tests (skip Docker setup)
|
||||
export SKIP_DOCKER_SETUP=true
|
||||
pnpm test tests/integration/
|
||||
```
|
||||
|
||||
⚠️ **Important:** Never run integration tests against production Mattermost instances. Tests create and delete data automatically.
|
||||
|
||||
#### Client Tests
|
||||
|
||||
Tests for the Mattermost API client:
|
||||
|
||||
```bash
|
||||
pnpm test src/mattermost/client.test.ts
|
||||
pnpm test src/mattermost/client.retry.test.ts
|
||||
```
|
||||
|
||||
#### WebSocket Tests
|
||||
|
||||
Tests for real-time message monitoring:
|
||||
|
||||
```bash
|
||||
pnpm test src/mattermost/monitor-websocket.test.ts
|
||||
```
|
||||
|
||||
### Test Environment Setup
|
||||
|
||||
#### Using Environment Variables
|
||||
|
||||
Create a `.env.test` file (not committed to git):
|
||||
|
||||
```bash
|
||||
# Test Mattermost instance
|
||||
MATTERMOST_TEST_URL=https://chat-test.example.com
|
||||
MATTERMOST_TEST_TOKEN=your-test-bot-token
|
||||
MATTERMOST_TEST_PAT=your-test-pat-optional
|
||||
|
||||
# Test configuration
|
||||
TEST_TIMEOUT=30000
|
||||
TEST_RETRY_COUNT=3
|
||||
```
|
||||
|
||||
#### Using Test Fixtures
|
||||
|
||||
Some tests use mock fixtures instead of real connections:
|
||||
|
||||
```typescript
|
||||
// Example: Using mock client
|
||||
import { createMockMattermostClient } from './test-helpers.js';
|
||||
|
||||
const mockClient = createMockMattermostClient({
|
||||
baseUrl: 'https://mock.example.com',
|
||||
botToken: 'mock-token'
|
||||
});
|
||||
```
|
||||
|
||||
### Coverage Reports
|
||||
|
||||
Generate and view coverage:
|
||||
|
||||
```bash
|
||||
# Generate coverage report
|
||||
pnpm test -- --coverage
|
||||
|
||||
# View HTML report
|
||||
open coverage/lcov-report/index.html
|
||||
|
||||
# Coverage thresholds (enforced in CI)
|
||||
# Statements: 80%
|
||||
# Branches: 75%
|
||||
# Functions: 80%
|
||||
# Lines: 80%
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Test File Organization
|
||||
|
||||
```
|
||||
extensions/mattermost/src/
|
||||
├── mattermost/
|
||||
│ ├── client.test.ts # Client unit tests
|
||||
│ ├── client.retry.test.ts # Retry logic tests
|
||||
│ ├── monitor-websocket.test.ts # WebSocket tests
|
||||
│ ├── interactions.test.ts # Interactive components tests
|
||||
│ └── ...
|
||||
├── config-schema.test.ts # Configuration validation
|
||||
├── security-audit.test.ts # Security audit tests
|
||||
├── errors.test.ts # Error handling tests
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Test Naming Conventions
|
||||
|
||||
- **File naming**: `*.test.ts` for unit tests, `*.integration.test.ts` for integration tests
|
||||
- **Describe blocks**: Use the name of the function/module being tested
|
||||
- **Test names**: Start with "should" and describe the expected behavior
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
describe('editMessage', () => {
|
||||
it('should successfully edit a bot message', async () => {
|
||||
// test code
|
||||
});
|
||||
|
||||
it('should fail when editing another users message', async () => {
|
||||
// test code
|
||||
});
|
||||
|
||||
it('should return PERMISSION_DENIED for system messages', async () => {
|
||||
// test code
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Unit Test Template
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { editMessage } from './actions.js';
|
||||
|
||||
describe('editMessage', () => {
|
||||
it('should edit a message successfully', async () => {
|
||||
// Arrange
|
||||
const mockClient = createMockClient();
|
||||
const params = {
|
||||
cfg: mockConfig,
|
||||
postId: 'post123',
|
||||
channelId: 'channel123',
|
||||
message: 'Updated'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = await editMessage(params);
|
||||
|
||||
// Assert
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.postId).toBe('post123');
|
||||
});
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
// Arrange
|
||||
const params = { /* invalid params */ };
|
||||
|
||||
// Act
|
||||
const result = await editMessage(params);
|
||||
|
||||
// Assert
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.errorCode).toBe('CONFIG_ERROR');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Error Handling
|
||||
|
||||
```typescript
|
||||
import { MattermostError, ErrorCodes } from './errors.js';
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should create structured error with context', () => {
|
||||
const error = new MattermostError({
|
||||
code: ErrorCodes.AUTHENTICATION_FAILED,
|
||||
message: 'Invalid token',
|
||||
context: {
|
||||
operation: 'test',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
expect(error.code).toBe(ErrorCodes.AUTHENTICATION_FAILED);
|
||||
expect(error.retryable).toBe(false);
|
||||
expect(error.toJSON()).toMatchObject({
|
||||
code: ErrorCodes.AUTHENTICATION_FAILED
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Retries
|
||||
|
||||
```typescript
|
||||
import { withRetry } from './errors.js';
|
||||
|
||||
describe('withRetry', () => {
|
||||
it('should retry on retryable errors', async () => {
|
||||
let attempts = 0;
|
||||
const operation = async () => {
|
||||
attempts++;
|
||||
if (attempts < 3) {
|
||||
throw new NetworkError('Connection failed', context);
|
||||
}
|
||||
return 'success';
|
||||
};
|
||||
|
||||
const result = await withRetry('test-op', operation, {
|
||||
maxRetries: 3,
|
||||
initialDelayMs: 10
|
||||
});
|
||||
|
||||
expect(result).toBe('success');
|
||||
expect(attempts).toBe(3);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Security Features
|
||||
|
||||
```typescript
|
||||
import { collectMattermostSecurityAuditFindings } from './security-audit.js';
|
||||
|
||||
describe('security audit', () => {
|
||||
it('should detect hardcoded tokens', async () => {
|
||||
const findings = await collectMattermostSecurityAuditFindings({
|
||||
cfg: mockConfig,
|
||||
accountId: 'test',
|
||||
account: {
|
||||
...mockAccount,
|
||||
botToken: 'hardcoded-token-123456789',
|
||||
botTokenSource: 'config'
|
||||
}
|
||||
});
|
||||
|
||||
const hardcodedFinding = findings.find(
|
||||
f => f.checkId === 'channels.mattermost.pat.hardcoded_token'
|
||||
);
|
||||
|
||||
expect(hardcodedFinding).toBeDefined();
|
||||
expect(hardcodedFinding?.severity).toBe('critical');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking the Mattermost Client
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Create mock client
|
||||
const createMockClient = () => ({
|
||||
request: vi.fn(),
|
||||
fetchImpl: vi.fn(),
|
||||
baseUrl: 'https://mock.example.com',
|
||||
apiBaseUrl: 'https://mock.example.com/api/v4',
|
||||
token: 'mock-token'
|
||||
});
|
||||
|
||||
// Mock module
|
||||
vi.mock('./client.js', () => ({
|
||||
createMattermostClient: vi.fn(() => createMockClient())
|
||||
}));
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### TypeScript Guidelines
|
||||
|
||||
- Use strict TypeScript (`strict: true` in tsconfig)
|
||||
- Explicit return types on public functions
|
||||
- Avoid `any` - use `unknown` with type guards
|
||||
- Use `readonly` for immutable properties
|
||||
|
||||
### Code Formatting
|
||||
|
||||
```bash
|
||||
# Check code style
|
||||
pnpm lint
|
||||
|
||||
# Fix auto-fixable issues
|
||||
pnpm lint --fix
|
||||
```
|
||||
|
||||
### Import Order
|
||||
|
||||
1. External dependencies (e.g., `openclaw/plugin-sdk`)
|
||||
2. Internal modules (relative imports)
|
||||
3. Types only imports
|
||||
|
||||
Example:
|
||||
|
||||
```typescript
|
||||
// 1. External
|
||||
import { z } from 'openclaw/plugin-sdk/zod';
|
||||
|
||||
// 2. Internal
|
||||
import { createMattermostClient } from './client.js';
|
||||
import type { MattermostAccountConfig } from './types.js';
|
||||
```
|
||||
|
||||
## Pull Request Process
|
||||
|
||||
### Before Submitting
|
||||
|
||||
1. **Run all tests:**
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
2. **Check type safety:**
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
3. **Run linter:**
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
4. **Build project:**
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
5. **Check test coverage:**
|
||||
```bash
|
||||
pnpm test -- --coverage
|
||||
```
|
||||
|
||||
### PR Requirements
|
||||
|
||||
- **Clear description** of changes
|
||||
- **Link to related issues**
|
||||
- **Tests included** for new features
|
||||
- **Documentation updated** if needed
|
||||
- **No breaking changes** (or clearly marked)
|
||||
- **CI checks passing**
|
||||
|
||||
### PR Template
|
||||
|
||||
```markdown
|
||||
## Description
|
||||
Brief description of the change
|
||||
|
||||
## Type of Change
|
||||
- [ ] Bug fix
|
||||
- [ ] New feature
|
||||
- [ ] Breaking change
|
||||
- [ ] Documentation update
|
||||
|
||||
## Testing
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Integration tests added/updated
|
||||
- [ ] All tests passing
|
||||
|
||||
## Checklist
|
||||
- [ ] Code follows style guidelines
|
||||
- [ ] Self-review completed
|
||||
- [ ] Documentation updated
|
||||
- [ ] No new warnings
|
||||
```
|
||||
|
||||
### Review Process
|
||||
|
||||
1. Automated checks must pass (CI, coverage)
|
||||
2. At least one maintainer review required
|
||||
3. Address review feedback promptly
|
||||
4. Squash commits before merge (if requested)
|
||||
|
||||
## Release Process
|
||||
|
||||
### Version Bump
|
||||
|
||||
```bash
|
||||
# Update version in package.json
|
||||
npm version patch # or minor, major
|
||||
```
|
||||
|
||||
### Changelog Update
|
||||
|
||||
Add entry to `CHANGELOG.md` following Keep a Changelog format:
|
||||
|
||||
```markdown
|
||||
## [1.1.0] - 2026-04-15
|
||||
|
||||
### Added
|
||||
- New feature description
|
||||
|
||||
### Fixed
|
||||
- Bug fix description
|
||||
```
|
||||
|
||||
### Release Steps
|
||||
|
||||
1. Update version in `package.json`
|
||||
2. Update `CHANGELOG.md`
|
||||
3. Create PR with version bump
|
||||
4. After merge, tag the release:
|
||||
```bash
|
||||
git tag -a v1.1.0 -m "Release v1.1.0"
|
||||
git push origin v1.1.0
|
||||
```
|
||||
5. CI will automatically publish to npm
|
||||
|
||||
## Questions?
|
||||
|
||||
- Open an issue on GitHub
|
||||
- Check existing issues and discussions
|
||||
- Review the [API Reference](docs/API.md)
|
||||
|
||||
Thank you for contributing!
|
||||
Reference in New Issue
Block a user