mirror of
https://github.com/caddyserver/caddy.git
synced 2025-10-20 00:03:17 +00:00
642 lines
16 KiB
Go
642 lines
16 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package caddy
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNewEvent_Basic(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
eventName := "test.event"
|
|
eventData := map[string]any{
|
|
"key1": "value1",
|
|
"key2": 42,
|
|
}
|
|
|
|
event, err := NewEvent(ctx, eventName, eventData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
// Verify event properties
|
|
if event.Name() != eventName {
|
|
t.Errorf("Expected name '%s', got '%s'", eventName, event.Name())
|
|
}
|
|
|
|
if event.Data == nil {
|
|
t.Error("Expected non-nil data")
|
|
}
|
|
|
|
if len(event.Data) != len(eventData) {
|
|
t.Errorf("Expected %d data items, got %d", len(eventData), len(event.Data))
|
|
}
|
|
|
|
for key, expectedValue := range eventData {
|
|
if actualValue, exists := event.Data[key]; !exists || actualValue != expectedValue {
|
|
t.Errorf("Data key '%s': expected %v, got %v", key, expectedValue, actualValue)
|
|
}
|
|
}
|
|
|
|
// Verify ID is generated
|
|
if event.ID().String() == "" {
|
|
t.Error("Event ID should not be empty")
|
|
}
|
|
|
|
// Verify timestamp is recent
|
|
if time.Since(event.Timestamp()) > time.Second {
|
|
t.Error("Event timestamp should be recent")
|
|
}
|
|
}
|
|
|
|
func TestNewEvent_NameNormalization(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"UPPERCASE", "uppercase"},
|
|
{"MixedCase", "mixedcase"},
|
|
{"already.lower", "already.lower"},
|
|
{"With-Dashes", "with-dashes"},
|
|
{"With_Underscores", "with_underscores"},
|
|
{"", ""},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.input, func(t *testing.T) {
|
|
event, err := NewEvent(ctx, test.input, nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
if event.Name() != test.expected {
|
|
t.Errorf("Expected normalized name '%s', got '%s'", test.expected, event.Name())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEvent_CloudEvent_NilData(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
event, err := NewEvent(ctx, "test", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
cloudEvent := event.CloudEvent()
|
|
|
|
// Should not panic with nil data
|
|
if cloudEvent.Data == nil {
|
|
t.Error("CloudEvent data should not be nil even with nil input")
|
|
}
|
|
|
|
// Should be valid JSON
|
|
var parsed any
|
|
if err := json.Unmarshal(cloudEvent.Data, &parsed); err != nil {
|
|
t.Errorf("CloudEvent data should be valid JSON: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEvent_CloudEvent_WithModule(t *testing.T) {
|
|
// Create a context with a mock module
|
|
mockMod := &mockModule{}
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
// Simulate module ancestry
|
|
ctx.ancestry = []Module{mockMod}
|
|
|
|
event, err := NewEvent(ctx, "test", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
cloudEvent := event.CloudEvent()
|
|
|
|
// Source should be the module ID
|
|
expectedSource := string(mockMod.CaddyModule().ID)
|
|
if cloudEvent.Source != expectedSource {
|
|
t.Errorf("Expected source '%s', got '%s'", expectedSource, cloudEvent.Source)
|
|
}
|
|
|
|
// Origin should be the module
|
|
if event.Origin() != mockMod {
|
|
t.Error("Expected event origin to be the mock module")
|
|
}
|
|
}
|
|
|
|
func TestEvent_CloudEvent_Fields(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
eventName := "test.event"
|
|
eventData := map[string]any{"test": "data"}
|
|
|
|
event, err := NewEvent(ctx, eventName, eventData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
cloudEvent := event.CloudEvent()
|
|
|
|
// Verify CloudEvent fields
|
|
if cloudEvent.ID == "" {
|
|
t.Error("CloudEvent ID should not be empty")
|
|
}
|
|
|
|
if cloudEvent.Source != "caddy" {
|
|
t.Errorf("Expected source 'caddy' for nil module, got '%s'", cloudEvent.Source)
|
|
}
|
|
|
|
if cloudEvent.SpecVersion != "1.0" {
|
|
t.Errorf("Expected spec version '1.0', got '%s'", cloudEvent.SpecVersion)
|
|
}
|
|
|
|
if cloudEvent.Type != eventName {
|
|
t.Errorf("Expected type '%s', got '%s'", eventName, cloudEvent.Type)
|
|
}
|
|
|
|
if cloudEvent.DataContentType != "application/json" {
|
|
t.Errorf("Expected content type 'application/json', got '%s'", cloudEvent.DataContentType)
|
|
}
|
|
|
|
// Verify data is valid JSON
|
|
var parsedData map[string]any
|
|
if err := json.Unmarshal(cloudEvent.Data, &parsedData); err != nil {
|
|
t.Errorf("CloudEvent data is not valid JSON: %v", err)
|
|
}
|
|
|
|
if parsedData["test"] != "data" {
|
|
t.Errorf("Expected data to contain test='data', got %v", parsedData)
|
|
}
|
|
}
|
|
|
|
func TestEvent_ConcurrentAccess(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
event, err := NewEvent(ctx, "concurrent.test", map[string]any{
|
|
"counter": 0,
|
|
"data": "shared",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
const numGoroutines = 50
|
|
var wg sync.WaitGroup
|
|
|
|
// Test concurrent read access to event properties
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
// These should be safe for concurrent access
|
|
_ = event.ID()
|
|
_ = event.Name()
|
|
_ = event.Timestamp()
|
|
_ = event.Origin()
|
|
_ = event.CloudEvent()
|
|
|
|
// Data map is not synchronized, so read-only access should be safe
|
|
if data, exists := event.Data["data"]; !exists || data != "shared" {
|
|
t.Errorf("Goroutine %d: Expected shared data", id)
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestEvent_DataModification_Warning(t *testing.T) {
|
|
// This test documents the non-thread-safe nature of event data
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
event, err := NewEvent(ctx, "data.test", map[string]any{
|
|
"mutable": "original",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
// Modifying data after creation (this is allowed but not thread-safe)
|
|
event.Data["mutable"] = "modified"
|
|
event.Data["new_key"] = "new_value"
|
|
|
|
// Verify modifications are visible
|
|
if event.Data["mutable"] != "modified" {
|
|
t.Error("Data modification should be visible")
|
|
}
|
|
if event.Data["new_key"] != "new_value" {
|
|
t.Error("New data should be visible")
|
|
}
|
|
|
|
// CloudEvent should reflect the current state
|
|
cloudEvent := event.CloudEvent()
|
|
var parsedData map[string]any
|
|
json.Unmarshal(cloudEvent.Data, &parsedData)
|
|
|
|
if parsedData["mutable"] != "modified" {
|
|
t.Error("CloudEvent should reflect modified data")
|
|
}
|
|
if parsedData["new_key"] != "new_value" {
|
|
t.Error("CloudEvent should reflect new data")
|
|
}
|
|
}
|
|
|
|
func TestEvent_Aborted_State(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
event, err := NewEvent(ctx, "abort.test", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
// Initially not aborted
|
|
if event.Aborted != nil {
|
|
t.Error("Event should not be aborted initially")
|
|
}
|
|
|
|
// Simulate aborting the event
|
|
event.Aborted = ErrEventAborted
|
|
|
|
if event.Aborted != ErrEventAborted {
|
|
t.Error("Event should be marked as aborted")
|
|
}
|
|
}
|
|
|
|
func TestErrEventAborted_Value(t *testing.T) {
|
|
if ErrEventAborted == nil {
|
|
t.Error("ErrEventAborted should not be nil")
|
|
}
|
|
|
|
if ErrEventAborted.Error() != "event aborted" {
|
|
t.Errorf("Expected 'event aborted', got '%s'", ErrEventAborted.Error())
|
|
}
|
|
}
|
|
|
|
func TestEvent_UniqueIDs(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
const numEvents = 1000
|
|
ids := make(map[string]bool)
|
|
|
|
for i := 0; i < numEvents; i++ {
|
|
event, err := NewEvent(ctx, "unique.test", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event %d: %v", i, err)
|
|
}
|
|
|
|
idStr := event.ID().String()
|
|
if ids[idStr] {
|
|
t.Errorf("Duplicate event ID: %s", idStr)
|
|
}
|
|
ids[idStr] = true
|
|
}
|
|
}
|
|
|
|
func TestEvent_TimestampProgression(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
// Create events with small delays
|
|
events := make([]Event, 5)
|
|
for i := range events {
|
|
var err error
|
|
events[i], err = NewEvent(ctx, "time.test", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event %d: %v", i, err)
|
|
}
|
|
|
|
if i < len(events)-1 {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// Verify timestamps are in ascending order
|
|
for i := 1; i < len(events); i++ {
|
|
if !events[i].Timestamp().After(events[i-1].Timestamp()) {
|
|
t.Errorf("Event %d timestamp (%v) should be after event %d timestamp (%v)",
|
|
i, events[i].Timestamp(), i-1, events[i-1].Timestamp())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEvent_JSON_Serialization(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
eventData := map[string]any{
|
|
"string": "value",
|
|
"number": 42,
|
|
"boolean": true,
|
|
"array": []any{1, 2, 3},
|
|
"object": map[string]any{"nested": "value"},
|
|
}
|
|
|
|
event, err := NewEvent(ctx, "json.test", eventData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
cloudEvent := event.CloudEvent()
|
|
|
|
// CloudEvent should be JSON serializable
|
|
cloudEventJSON, err := json.Marshal(cloudEvent)
|
|
if err != nil {
|
|
t.Fatalf("Failed to marshal CloudEvent: %v", err)
|
|
}
|
|
|
|
// Should be able to unmarshal back
|
|
var parsed CloudEvent
|
|
err = json.Unmarshal(cloudEventJSON, &parsed)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal CloudEvent: %v", err)
|
|
}
|
|
|
|
// Verify key fields survived round-trip
|
|
if parsed.ID != cloudEvent.ID {
|
|
t.Errorf("ID mismatch after round-trip")
|
|
}
|
|
if parsed.Source != cloudEvent.Source {
|
|
t.Errorf("Source mismatch after round-trip")
|
|
}
|
|
if parsed.Type != cloudEvent.Type {
|
|
t.Errorf("Type mismatch after round-trip")
|
|
}
|
|
}
|
|
|
|
func TestEvent_EmptyData(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
// Test with empty map
|
|
event1, err := NewEvent(ctx, "empty.map", map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event with empty map: %v", err)
|
|
}
|
|
|
|
cloudEvent1 := event1.CloudEvent()
|
|
var parsed1 map[string]any
|
|
json.Unmarshal(cloudEvent1.Data, &parsed1)
|
|
if len(parsed1) != 0 {
|
|
t.Error("Expected empty data map")
|
|
}
|
|
|
|
// Test with nil data
|
|
event2, err := NewEvent(ctx, "nil.data", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event with nil data: %v", err)
|
|
}
|
|
|
|
cloudEvent2 := event2.CloudEvent()
|
|
if cloudEvent2.Data == nil {
|
|
t.Error("CloudEvent data should not be nil even with nil input")
|
|
}
|
|
}
|
|
|
|
func TestEvent_Origin_WithModule(t *testing.T) {
|
|
mockMod := &mockEventModule{}
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
// Set module in ancestry
|
|
ctx.ancestry = []Module{mockMod}
|
|
|
|
event, err := NewEvent(ctx, "module.test", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
if event.Origin() != mockMod {
|
|
t.Error("Expected event origin to be the mock module")
|
|
}
|
|
|
|
cloudEvent := event.CloudEvent()
|
|
expectedSource := string(mockMod.CaddyModule().ID)
|
|
if cloudEvent.Source != expectedSource {
|
|
t.Errorf("Expected source '%s', got '%s'", expectedSource, cloudEvent.Source)
|
|
}
|
|
}
|
|
|
|
func TestEvent_LargeData(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
// Create event with large data
|
|
largeData := make(map[string]any)
|
|
for i := 0; i < 1000; i++ {
|
|
largeData[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
|
|
}
|
|
|
|
event, err := NewEvent(ctx, "large.data", largeData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event with large data: %v", err)
|
|
}
|
|
|
|
// CloudEvent should handle large data
|
|
cloudEvent := event.CloudEvent()
|
|
|
|
var parsedData map[string]any
|
|
err = json.Unmarshal(cloudEvent.Data, &parsedData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse large data in CloudEvent: %v", err)
|
|
}
|
|
|
|
if len(parsedData) != len(largeData) {
|
|
t.Errorf("Expected %d data items, got %d", len(largeData), len(parsedData))
|
|
}
|
|
}
|
|
|
|
func TestEvent_SpecialCharacters_InData(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
specialData := map[string]any{
|
|
"unicode": "🚀✨",
|
|
"newlines": "line1\nline2\r\nline3",
|
|
"quotes": `"double" and 'single' quotes`,
|
|
"backslashes": "\\path\\to\\file",
|
|
"json_chars": `{"key": "value"}`,
|
|
"empty": "",
|
|
"null_value": nil,
|
|
}
|
|
|
|
event, err := NewEvent(ctx, "special.chars", specialData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
cloudEvent := event.CloudEvent()
|
|
|
|
// Should produce valid JSON
|
|
var parsedData map[string]any
|
|
err = json.Unmarshal(cloudEvent.Data, &parsedData)
|
|
if err != nil {
|
|
t.Fatalf("Failed to parse data with special characters: %v", err)
|
|
}
|
|
|
|
// Verify some special cases survived JSON round-trip
|
|
if parsedData["unicode"] != "🚀✨" {
|
|
t.Error("Unicode characters should survive JSON encoding")
|
|
}
|
|
|
|
if parsedData["quotes"] != `"double" and 'single' quotes` {
|
|
t.Error("Quotes should be properly escaped in JSON")
|
|
}
|
|
}
|
|
|
|
func TestEvent_ConcurrentCreation(t *testing.T) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
const numGoroutines = 100
|
|
var wg sync.WaitGroup
|
|
events := make([]Event, numGoroutines)
|
|
errors := make([]error, numGoroutines)
|
|
|
|
// Create events concurrently
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(index int) {
|
|
defer wg.Done()
|
|
|
|
eventData := map[string]any{
|
|
"goroutine": index,
|
|
"timestamp": time.Now().UnixNano(),
|
|
}
|
|
|
|
events[index], errors[index] = NewEvent(ctx, "concurrent.test", eventData)
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Verify all events were created successfully
|
|
ids := make(map[string]bool)
|
|
for i, event := range events {
|
|
if errors[i] != nil {
|
|
t.Errorf("Goroutine %d: Failed to create event: %v", i, errors[i])
|
|
continue
|
|
}
|
|
|
|
// Verify unique IDs
|
|
idStr := event.ID().String()
|
|
if ids[idStr] {
|
|
t.Errorf("Duplicate event ID: %s", idStr)
|
|
}
|
|
ids[idStr] = true
|
|
|
|
// Verify data integrity
|
|
if goroutineID, exists := event.Data["goroutine"]; !exists || goroutineID != i {
|
|
t.Errorf("Event %d: Data corruption detected", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mock module for event testing
|
|
type mockEventModule struct{}
|
|
|
|
func (m *mockEventModule) CaddyModule() ModuleInfo {
|
|
return ModuleInfo{
|
|
ID: "test.event.module",
|
|
New: func() Module { return new(mockEventModule) },
|
|
}
|
|
}
|
|
|
|
func TestEvent_TimeAccuracy(t *testing.T) {
|
|
before := time.Now()
|
|
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
event, err := NewEvent(ctx, "time.accuracy", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create event: %v", err)
|
|
}
|
|
|
|
after := time.Now()
|
|
eventTime := event.Timestamp()
|
|
|
|
// Event timestamp should be between before and after
|
|
if eventTime.Before(before) || eventTime.After(after) {
|
|
t.Errorf("Event timestamp %v should be between %v and %v", eventTime, before, after)
|
|
}
|
|
}
|
|
|
|
func BenchmarkNewEvent(b *testing.B) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
eventData := map[string]any{
|
|
"key1": "value1",
|
|
"key2": 42,
|
|
"key3": true,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
NewEvent(ctx, "benchmark.test", eventData)
|
|
}
|
|
}
|
|
|
|
func BenchmarkEvent_CloudEvent(b *testing.B) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
event, _ := NewEvent(ctx, "benchmark.cloud", map[string]any{
|
|
"data": "test",
|
|
"num": 123,
|
|
})
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
event.CloudEvent()
|
|
}
|
|
}
|
|
|
|
func BenchmarkEvent_CloudEvent_LargeData(b *testing.B) {
|
|
ctx, cancel := NewContext(Context{Context: context.Background()})
|
|
defer cancel()
|
|
|
|
// Create event with substantial data
|
|
largeData := make(map[string]any)
|
|
for i := 0; i < 100; i++ {
|
|
largeData[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
|
|
}
|
|
|
|
event, _ := NewEvent(ctx, "benchmark.large", largeData)
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
event.CloudEvent()
|
|
}
|
|
}
|