feat: add AUTH_TOKEN_FILE support for Docker secrets (v2.7.5)
- Add AUTH_TOKEN_FILE environment variable support for reading auth tokens from files - Support Docker secrets pattern for production deployments - Add Known Issues section documenting Claude Desktop container duplication bug - Update documentation with authentication options and best practices - Fix issue #16: AUTH_TOKEN_FILE was documented but not implemented - Add comprehensive tests for AUTH_TOKEN_FILE functionality BREAKING CHANGE: None - AUTH_TOKEN continues to work as before 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
10
CLAUDE.md
10
CLAUDE.md
@@ -6,7 +6,15 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
|
|
||||||
n8n-mcp is a comprehensive documentation and knowledge server that provides AI assistants with complete access to n8n node information through the Model Context Protocol (MCP). It serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively.
|
n8n-mcp is a comprehensive documentation and knowledge server that provides AI assistants with complete access to n8n node information through the Model Context Protocol (MCP). It serves as a bridge between n8n's workflow automation platform and AI models, enabling them to understand and work with n8n nodes effectively.
|
||||||
|
|
||||||
## ✅ Latest Updates (v2.7.4)
|
## ✅ Latest Updates (v2.7.5)
|
||||||
|
|
||||||
|
### Update (v2.7.5) - AUTH_TOKEN_FILE Support & Known Issues:
|
||||||
|
- ✅ **NEW: AUTH_TOKEN_FILE support** - Read authentication token from file (Docker secrets compatible)
|
||||||
|
- ✅ **ADDED: Known Issues section** - Documented Claude Desktop container duplication bug
|
||||||
|
- ✅ **ENHANCED: Authentication flexibility** - Support both AUTH_TOKEN and AUTH_TOKEN_FILE variables
|
||||||
|
- ✅ **FIXED: Issue #16** - AUTH_TOKEN_FILE now properly implemented as documented
|
||||||
|
- ✅ **DOCKER SECRETS**: Seamlessly integrate with Docker secrets management
|
||||||
|
- ✅ **BACKWARD COMPATIBLE**: AUTH_TOKEN continues to work as before
|
||||||
|
|
||||||
### Update (v2.7.4) - Self-Documenting MCP Tools:
|
### Update (v2.7.4) - Self-Documenting MCP Tools:
|
||||||
- ✅ **RENAMED: start_here_workflow_guide → tools_documentation** - More descriptive name
|
- ✅ **RENAMED: start_here_workflow_guide → tools_documentation** - More descriptive name
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -546,6 +546,42 @@ Current database coverage (n8n v1.99.1):
|
|||||||
|
|
||||||
See [CHANGELOG.md](./docs/CHANGELOG.md) for full version history.
|
See [CHANGELOG.md](./docs/CHANGELOG.md) for full version history.
|
||||||
|
|
||||||
|
## ⚠️ Known Issues
|
||||||
|
|
||||||
|
### Claude Desktop Container Duplication
|
||||||
|
When using n8n-MCP with Claude Desktop in Docker mode, Claude Desktop may start the container twice during initialization. This is a known Claude Desktop bug ([modelcontextprotocol/servers#812](https://github.com/modelcontextprotocol/servers/issues/812)).
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- Two identical containers running for the same MCP server
|
||||||
|
- Container name conflicts if using `--name` parameter
|
||||||
|
- Doubled resource usage
|
||||||
|
|
||||||
|
**Workarounds:**
|
||||||
|
1. **Avoid using --name parameter** - Let Docker assign random names:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"n8n-mcp": {
|
||||||
|
"command": "docker",
|
||||||
|
"args": [
|
||||||
|
"run", "-i", "--rm",
|
||||||
|
"ghcr.io/czlonkowski/n8n-mcp:latest"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Use HTTP mode instead** - Deploy n8n-mcp as a standalone HTTP server:
|
||||||
|
```bash
|
||||||
|
docker compose up -d # Start HTTP server
|
||||||
|
```
|
||||||
|
Then connect via mcp-remote (see [HTTP Deployment Guide](./docs/HTTP_DEPLOYMENT.md))
|
||||||
|
|
||||||
|
3. **Use Docker MCP Toolkit** - Better container management through Docker Desktop
|
||||||
|
|
||||||
|
This issue does not affect the functionality of n8n-MCP itself, only the container management in Claude Desktop.
|
||||||
|
|
||||||
## 📦 License
|
## 📦 License
|
||||||
|
|
||||||
MIT License - see [LICENSE](LICENSE) for details.
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|||||||
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [2.7.5] - 2025-07-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- AUTH_TOKEN_FILE support for reading authentication tokens from files (Docker secrets compatible)
|
||||||
|
- Known Issues section in README documenting Claude Desktop container duplication bug
|
||||||
|
- Enhanced authentication documentation in Docker README
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Issue #16: AUTH_TOKEN_FILE was documented but not implemented
|
||||||
|
- HTTP server now properly supports both AUTH_TOKEN and AUTH_TOKEN_FILE environment variables
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Authentication logic now checks AUTH_TOKEN first, then falls back to AUTH_TOKEN_FILE
|
||||||
|
- Updated Docker documentation to clarify authentication options
|
||||||
|
|
||||||
## [2.7.4] - 2025-07-03
|
## [2.7.4] - 2025-07-03
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -59,11 +59,14 @@ docker run -d \
|
|||||||
| Variable | Description | Default | Required |
|
| Variable | Description | Default | Required |
|
||||||
|----------|-------------|---------|----------|
|
|----------|-------------|---------|----------|
|
||||||
| `MCP_MODE` | Server mode: `stdio` or `http` | `stdio` | No |
|
| `MCP_MODE` | Server mode: `stdio` or `http` | `stdio` | No |
|
||||||
| `AUTH_TOKEN` | Bearer token for HTTP authentication | - | Yes (HTTP mode) |
|
| `AUTH_TOKEN` | Bearer token for HTTP authentication | - | Yes (HTTP mode)* |
|
||||||
|
| `AUTH_TOKEN_FILE` | Path to file containing auth token (v2.7.5+) | - | Yes (HTTP mode)* |
|
||||||
| `PORT` | HTTP server port | `3000` | No |
|
| `PORT` | HTTP server port | `3000` | No |
|
||||||
| `NODE_ENV` | Environment: `development` or `production` | `production` | No |
|
| `NODE_ENV` | Environment: `development` or `production` | `production` | No |
|
||||||
| `LOG_LEVEL` | Logging level: `debug`, `info`, `warn`, `error` | `info` | No |
|
| `LOG_LEVEL` | Logging level: `debug`, `info`, `warn`, `error` | `info` | No |
|
||||||
|
|
||||||
|
*Either `AUTH_TOKEN` or `AUTH_TOKEN_FILE` must be set for HTTP mode. If both are set, `AUTH_TOKEN` takes precedence.
|
||||||
|
|
||||||
### Docker Compose Configuration
|
### Docker Compose Configuration
|
||||||
|
|
||||||
The default `docker-compose.yml` provides:
|
The default `docker-compose.yml` provides:
|
||||||
@@ -238,18 +241,40 @@ docker inspect n8n-mcp | jq '.[0].State.Health'
|
|||||||
|
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
- Always use a strong AUTH_TOKEN (minimum 32 characters)
|
n8n-MCP supports two authentication methods for HTTP mode:
|
||||||
- Never commit tokens to version control
|
|
||||||
- Rotate tokens regularly
|
#### Method 1: AUTH_TOKEN (Environment Variable)
|
||||||
|
- Set the token directly as an environment variable
|
||||||
|
- Simple and straightforward for basic deployments
|
||||||
|
- Always use a strong token (minimum 32 characters)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Generate secure token
|
# Generate secure token
|
||||||
openssl rand -base64 32
|
openssl rand -base64 32
|
||||||
|
|
||||||
# Or use uuidgen
|
# Use in Docker
|
||||||
uuidgen | tr -d '-' | base64
|
docker run -e AUTH_TOKEN=your-secure-token ...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Method 2: AUTH_TOKEN_FILE (File Path) - NEW in v2.7.5
|
||||||
|
- Read token from a file (Docker secrets compatible)
|
||||||
|
- More secure for production deployments
|
||||||
|
- Prevents token exposure in process lists
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create token file
|
||||||
|
echo "your-secure-token" > /path/to/token.txt
|
||||||
|
|
||||||
|
# Use with Docker secrets
|
||||||
|
docker run -e AUTH_TOKEN_FILE=/run/secrets/auth_token ...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Best Practices
|
||||||
|
- Never commit tokens to version control
|
||||||
|
- Rotate tokens regularly
|
||||||
|
- Use AUTH_TOKEN_FILE with Docker secrets for production
|
||||||
|
- Ensure token files have restricted permissions (600)
|
||||||
|
|
||||||
### Network Security
|
### Network Security
|
||||||
|
|
||||||
For production deployments:
|
For production deployments:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "n8n-mcp",
|
"name": "n8n-mcp",
|
||||||
"version": "2.7.4",
|
"version": "2.7.5",
|
||||||
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
"description": "Integration between n8n workflow automation and Model Context Protocol (MCP)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -12,26 +12,56 @@ import { logger } from './utils/logger';
|
|||||||
import { PROJECT_VERSION } from './utils/version';
|
import { PROJECT_VERSION } from './utils/version';
|
||||||
import { isN8nApiConfigured } from './config/n8n-api';
|
import { isN8nApiConfigured } from './config/n8n-api';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
let expressServer: any;
|
let expressServer: any;
|
||||||
|
let authToken: string | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load auth token from environment variable or file
|
||||||
|
*/
|
||||||
|
export function loadAuthToken(): string | null {
|
||||||
|
// First, try AUTH_TOKEN environment variable
|
||||||
|
if (process.env.AUTH_TOKEN) {
|
||||||
|
logger.info('Using AUTH_TOKEN from environment variable');
|
||||||
|
return process.env.AUTH_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then, try AUTH_TOKEN_FILE
|
||||||
|
if (process.env.AUTH_TOKEN_FILE) {
|
||||||
|
try {
|
||||||
|
const token = readFileSync(process.env.AUTH_TOKEN_FILE, 'utf-8').trim();
|
||||||
|
logger.info(`Loaded AUTH_TOKEN from file: ${process.env.AUTH_TOKEN_FILE}`);
|
||||||
|
return token;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to read AUTH_TOKEN_FILE: ${process.env.AUTH_TOKEN_FILE}`, error);
|
||||||
|
console.error(`ERROR: Failed to read AUTH_TOKEN_FILE: ${process.env.AUTH_TOKEN_FILE}`);
|
||||||
|
console.error(error instanceof Error ? error.message : 'Unknown error');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate required environment variables
|
* Validate required environment variables
|
||||||
*/
|
*/
|
||||||
function validateEnvironment() {
|
function validateEnvironment() {
|
||||||
const required = ['AUTH_TOKEN'];
|
// Load auth token from env var or file
|
||||||
const missing = required.filter(key => !process.env[key]);
|
authToken = loadAuthToken();
|
||||||
|
|
||||||
if (missing.length > 0) {
|
if (!authToken) {
|
||||||
logger.error(`Missing required environment variables: ${missing.join(', ')}`);
|
logger.error('No authentication token found');
|
||||||
console.error(`ERROR: Missing required environment variables: ${missing.join(', ')}`);
|
console.error('ERROR: AUTH_TOKEN is required for HTTP mode');
|
||||||
|
console.error('Set AUTH_TOKEN environment variable or AUTH_TOKEN_FILE pointing to a file containing the token');
|
||||||
console.error('Generate AUTH_TOKEN with: openssl rand -base64 32');
|
console.error('Generate AUTH_TOKEN with: openssl rand -base64 32');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.AUTH_TOKEN && process.env.AUTH_TOKEN.length < 32) {
|
if (authToken.length < 32) {
|
||||||
logger.warn('AUTH_TOKEN should be at least 32 characters for security');
|
logger.warn('AUTH_TOKEN should be at least 32 characters for security');
|
||||||
console.warn('WARNING: AUTH_TOKEN should be at least 32 characters for security');
|
console.warn('WARNING: AUTH_TOKEN should be at least 32 characters for security');
|
||||||
}
|
}
|
||||||
@@ -151,7 +181,7 @@ export async function startFixedHTTPServer() {
|
|||||||
? authHeader.slice(7)
|
? authHeader.slice(7)
|
||||||
: authHeader;
|
: authHeader;
|
||||||
|
|
||||||
if (token !== process.env.AUTH_TOKEN) {
|
if (token !== authToken) {
|
||||||
logger.warn('Authentication failed', {
|
logger.warn('Authentication failed', {
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
userAgent: req.get('user-agent')
|
userAgent: req.get('user-agent')
|
||||||
|
|||||||
266
tests/http-server-auth.test.ts
Normal file
266
tests/http-server-auth.test.ts
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { tmpdir } from 'os';
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
jest.mock('../src/utils/logger', () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
debug: jest.fn()
|
||||||
|
},
|
||||||
|
Logger: jest.fn().mockImplementation(() => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
debug: jest.fn()
|
||||||
|
})),
|
||||||
|
LogLevel: {
|
||||||
|
ERROR: 0,
|
||||||
|
WARN: 1,
|
||||||
|
INFO: 2,
|
||||||
|
DEBUG: 3
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('dotenv');
|
||||||
|
|
||||||
|
// Mock other dependencies to prevent side effects
|
||||||
|
jest.mock('../src/mcp/server', () => ({
|
||||||
|
N8NDocumentationMCPServer: jest.fn().mockImplementation(() => ({
|
||||||
|
executeTool: jest.fn()
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../src/mcp/tools', () => ({
|
||||||
|
n8nDocumentationToolsFinal: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../src/mcp/tools-n8n-manager', () => ({
|
||||||
|
n8nManagementTools: []
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../src/utils/version', () => ({
|
||||||
|
PROJECT_VERSION: '2.7.4'
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../src/config/n8n-api', () => ({
|
||||||
|
isN8nApiConfigured: jest.fn().mockReturnValue(false)
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock Express to prevent server from starting
|
||||||
|
jest.mock('express', () => {
|
||||||
|
const mockApp = {
|
||||||
|
use: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
post: jest.fn(),
|
||||||
|
listen: jest.fn().mockReturnValue({
|
||||||
|
on: jest.fn()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
const express: any = jest.fn(() => mockApp);
|
||||||
|
express.json = jest.fn();
|
||||||
|
express.urlencoded = jest.fn();
|
||||||
|
express.static = jest.fn();
|
||||||
|
express.Request = {};
|
||||||
|
express.Response = {};
|
||||||
|
express.NextFunction = {};
|
||||||
|
return express;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('HTTP Server Authentication', () => {
|
||||||
|
const originalEnv = process.env;
|
||||||
|
let tempDir: string;
|
||||||
|
let authTokenFile: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset modules and environment
|
||||||
|
jest.resetModules();
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
|
||||||
|
// Create temporary directory for test files
|
||||||
|
tempDir = join(tmpdir(), `http-server-auth-test-${Date.now()}`);
|
||||||
|
mkdirSync(tempDir, { recursive: true });
|
||||||
|
authTokenFile = join(tempDir, 'auth-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original environment
|
||||||
|
process.env = originalEnv;
|
||||||
|
|
||||||
|
// Clean up temporary directory
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('loadAuthToken', () => {
|
||||||
|
let loadAuthToken: () => string | null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Import the function after environment is set up
|
||||||
|
const httpServerModule = require('../src/http-server');
|
||||||
|
// Access the loadAuthToken function (we'll need to export it)
|
||||||
|
loadAuthToken = httpServerModule.loadAuthToken || (() => null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load token from AUTH_TOKEN environment variable', () => {
|
||||||
|
process.env.AUTH_TOKEN = 'test-token-from-env';
|
||||||
|
delete process.env.AUTH_TOKEN_FILE;
|
||||||
|
|
||||||
|
// Re-import to get fresh module with new env
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadAuthToken } = require('../src/http-server');
|
||||||
|
|
||||||
|
const token = loadAuthToken();
|
||||||
|
expect(token).toBe('test-token-from-env');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load token from AUTH_TOKEN_FILE when AUTH_TOKEN is not set', () => {
|
||||||
|
delete process.env.AUTH_TOKEN;
|
||||||
|
process.env.AUTH_TOKEN_FILE = authTokenFile;
|
||||||
|
|
||||||
|
// Write test token to file
|
||||||
|
writeFileSync(authTokenFile, 'test-token-from-file\n');
|
||||||
|
|
||||||
|
// Re-import to get fresh module with new env
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadAuthToken } = require('../src/http-server');
|
||||||
|
|
||||||
|
const token = loadAuthToken();
|
||||||
|
expect(token).toBe('test-token-from-file');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trim whitespace from token file', () => {
|
||||||
|
delete process.env.AUTH_TOKEN;
|
||||||
|
process.env.AUTH_TOKEN_FILE = authTokenFile;
|
||||||
|
|
||||||
|
// Write token with whitespace
|
||||||
|
writeFileSync(authTokenFile, ' test-token-with-spaces \n\n');
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadAuthToken } = require('../src/http-server');
|
||||||
|
|
||||||
|
const token = loadAuthToken();
|
||||||
|
expect(token).toBe('test-token-with-spaces');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer AUTH_TOKEN over AUTH_TOKEN_FILE', () => {
|
||||||
|
process.env.AUTH_TOKEN = 'env-token';
|
||||||
|
process.env.AUTH_TOKEN_FILE = authTokenFile;
|
||||||
|
writeFileSync(authTokenFile, 'file-token');
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadAuthToken } = require('../src/http-server');
|
||||||
|
|
||||||
|
const token = loadAuthToken();
|
||||||
|
expect(token).toBe('env-token');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when AUTH_TOKEN_FILE points to non-existent file', () => {
|
||||||
|
delete process.env.AUTH_TOKEN;
|
||||||
|
process.env.AUTH_TOKEN_FILE = join(tempDir, 'non-existent-file');
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadAuthToken } = require('../src/http-server');
|
||||||
|
const { logger } = require('../src/utils/logger');
|
||||||
|
|
||||||
|
const token = loadAuthToken();
|
||||||
|
expect(token).toBeNull();
|
||||||
|
expect(logger.error).toHaveBeenCalled();
|
||||||
|
const errorCall = logger.error.mock.calls[0];
|
||||||
|
expect(errorCall[0]).toContain('Failed to read AUTH_TOKEN_FILE');
|
||||||
|
expect(errorCall[1]).toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null when neither AUTH_TOKEN nor AUTH_TOKEN_FILE is set', () => {
|
||||||
|
delete process.env.AUTH_TOKEN;
|
||||||
|
delete process.env.AUTH_TOKEN_FILE;
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadAuthToken } = require('../src/http-server');
|
||||||
|
|
||||||
|
const token = loadAuthToken();
|
||||||
|
expect(token).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validateEnvironment', () => {
|
||||||
|
it('should exit when no auth token is available', () => {
|
||||||
|
delete process.env.AUTH_TOKEN;
|
||||||
|
delete process.env.AUTH_TOKEN_FILE;
|
||||||
|
|
||||||
|
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
||||||
|
throw new Error('Process exited');
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
require('../src/http-server');
|
||||||
|
}).toThrow('Process exited');
|
||||||
|
|
||||||
|
expect(mockExit).toHaveBeenCalledWith(1);
|
||||||
|
mockExit.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should warn when token is less than 32 characters', () => {
|
||||||
|
process.env.AUTH_TOKEN = 'short-token';
|
||||||
|
|
||||||
|
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
|
||||||
|
throw new Error('Process exited');
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
const { logger } = require('../src/utils/logger');
|
||||||
|
|
||||||
|
try {
|
||||||
|
require('../src/http-server');
|
||||||
|
} catch (error) {
|
||||||
|
// Module loads but may fail on server start
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(logger.warn).toHaveBeenCalledWith(
|
||||||
|
'AUTH_TOKEN should be at least 32 characters for security'
|
||||||
|
);
|
||||||
|
|
||||||
|
mockExit.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Integration test scenarios', () => {
|
||||||
|
it('should successfully authenticate with token from file', () => {
|
||||||
|
// This is more of an integration test placeholder
|
||||||
|
// In a real scenario, you'd start the server and make HTTP requests
|
||||||
|
|
||||||
|
writeFileSync(authTokenFile, 'very-secure-token-with-more-than-32-characters');
|
||||||
|
process.env.AUTH_TOKEN_FILE = authTokenFile;
|
||||||
|
delete process.env.AUTH_TOKEN;
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadAuthToken } = require('../src/http-server');
|
||||||
|
|
||||||
|
const token = loadAuthToken();
|
||||||
|
expect(token).toBe('very-secure-token-with-more-than-32-characters');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Docker secrets pattern', () => {
|
||||||
|
// Docker secrets are typically mounted at /run/secrets/
|
||||||
|
const dockerSecretPath = join(tempDir, 'run', 'secrets', 'auth_token');
|
||||||
|
mkdirSync(join(tempDir, 'run', 'secrets'), { recursive: true });
|
||||||
|
writeFileSync(dockerSecretPath, 'docker-secret-token');
|
||||||
|
|
||||||
|
process.env.AUTH_TOKEN_FILE = dockerSecretPath;
|
||||||
|
delete process.env.AUTH_TOKEN;
|
||||||
|
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadAuthToken } = require('../src/http-server');
|
||||||
|
|
||||||
|
const token = loadAuthToken();
|
||||||
|
expect(token).toBe('docker-secret-token');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user