Files
bruno 1a4808f5f7
CI / test (push) Has been cancelled
feat: v0.6.0 — ACLs multi-utilisateurs + Plugin system
**ACLs (Access Control Lists):**
- Ajout du type AclConfig dans sources/event.v (allowed_ips CIDR + allowed_tokens)
- Chaque source webhook (Gitea, Uptime Kuma, Cron, Generic) supporte les ACLs
- Validation IP via X-Forwarded-For / X-Real-IP avant HMAC
- Validation Bearer token via Authorization header
- Tests: 7 tests ACL (IP exact, CIDR, parse IPv4, ACL vide, IP+token combiné)

**Plugin system:**
- sources/plugin.v: runner exécutable externe, stdout JSON → Event
- Exit 0 = publish, exit ≠ 0 = skip. Timeout configurable
- Plugin loop dans server.v (goroutine, toutes les 60s)
- Example: scripts/example-plugin-disk.sh (vérifie espace disque)

**Docs:**
- README.md: ajout source Plugin + section Features complète
- ARCHITECTURE.md: flux Plugin, flux ACL, endpoints /metrics /api/silence
- ROADMAP.md: Phase 5 → 8/8 complet, ajout v0.6.0
- ntfy-bridge.example.yaml: sections ACLs et Plugins commentées
- Version bump: 0.5.0 → 0.6.0
2026-08-04 22:25:48 -04:00

106 lines
2.7 KiB
V

module main
import net.http
import sources
fn test_ip_matches_exact() {
assert ip_matches('192.168.30.5', '192.168.30.5')
assert !ip_matches('192.168.30.6', '192.168.30.5')
}
fn test_ip_matches_cidr() {
assert ip_matches('192.168.30.5', '192.168.0.0/16')
assert ip_matches('10.0.0.1', '10.0.0.0/8')
assert ip_matches('10.255.255.255', '10.0.0.0/8')
assert !ip_matches('11.0.0.1', '10.0.0.0/8')
assert ip_matches('192.168.1.100', '192.168.1.0/24')
assert !ip_matches('192.168.2.1', '192.168.1.0/24')
}
fn test_parse_ip_valid() {
octets := parse_ip('192.168.1.5')!
assert octets[0] == 192
assert octets[1] == 168
assert octets[2] == 1
assert octets[3] == 5
}
fn test_parse_ip_invalid() {
// parse_ip with invalid input should return an error
result := parse_ip('invalid') or { []u8{} }
assert result.len == 0
result2 := parse_ip('1.2.3') or { []u8{} }
assert result2.len == 0
result3 := parse_ip('999.1.2.3') or { []u8{} }
assert result3.len == 0
}
fn test_check_acl_no_acl() {
mut app := new_test_acl_app()
route := WebhookRoute{kind: .generic, index: 0}
assert app.check_acl(route, '1.2.3.4', '') == true
}
fn test_check_acl_ip_allow() {
mut app := new_test_acl_app()
app.cfg.sources.generic << sources.GenericSource{
name: 'test-source'
webhook_path: '/test'
topic: 'test-topic'
acl: sources.AclConfig{
allowed_ips: ['10.0.0.0/8']
}
}
route := WebhookRoute{kind: .generic, index: 0}
assert app.check_acl(route, '10.0.0.5', '') == true
assert app.check_acl(route, '10.255.0.1', '') == true
assert app.check_acl(route, '192.168.1.1', '') == false
assert app.check_acl(route, '', '') == false
}
fn test_check_acl_combined_ip_and_token() {
mut app := new_test_acl_app()
app.cfg.sources.generic << sources.GenericSource{
name: 'test-source'
webhook_path: '/test'
topic: 'test-topic'
acl: sources.AclConfig{
allowed_ips: ['10.0.0.0/8']
allowed_tokens: ['secret123']
}
}
route := WebhookRoute{kind: .generic, index: 0}
// Both pass
assert app.check_acl(route, '10.0.0.5', 'secret123') == true
// IP ok but no token
assert app.check_acl(route, '10.0.0.5', '') == false
// Token ok but wrong IP
assert app.check_acl(route, '192.168.1.1', 'secret123') == false
// Both wrong
assert app.check_acl(route, '1.1.1.1', 'wrong') == false
}
fn test_extract_bearer_token_empty() {
req := http.Request{
header: http.new_header()
}
assert extract_bearer_token(req) == ''
}
// Helper: create a minimal App for ACL tests
fn new_test_acl_app() App {
return App{
logger: new_logger(.info, false)
cfg: Config{
filters: FilterConfig{}
outgoing: OutgoingConfig{}
state: StateConfig{}
}
poll_state: map[string]bool{}
}
}