add tests
This commit is contained in:
633
tests/unit/service/cache_service_test.go
Normal file
633
tests/unit/service/cache_service_test.go
Normal file
@@ -0,0 +1,633 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"acc-server-manager/local/model"
|
||||
"acc-server-manager/local/utl/cache"
|
||||
"acc-server-manager/tests"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestInMemoryCache_Set_Get_Success(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test data
|
||||
key := "test-key"
|
||||
value := "test-value"
|
||||
duration := 5 * time.Minute
|
||||
|
||||
// Set value in cache
|
||||
c.Set(key, value, duration)
|
||||
|
||||
// Get value from cache
|
||||
result, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, value, result)
|
||||
}
|
||||
|
||||
func TestInMemoryCache_Get_NotFound(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Try to get non-existent key
|
||||
result, found := c.Get("non-existent-key")
|
||||
tests.AssertEqual(t, false, found)
|
||||
if result != nil {
|
||||
t.Fatal("Expected nil result, got non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInMemoryCache_Set_Get_NoExpiration(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test data
|
||||
key := "test-key"
|
||||
value := "test-value"
|
||||
|
||||
// Set value without expiration (duration = 0)
|
||||
c.Set(key, value, 0)
|
||||
|
||||
// Get value from cache
|
||||
result, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, value, result)
|
||||
}
|
||||
|
||||
func TestInMemoryCache_Expiration(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test data
|
||||
key := "test-key"
|
||||
value := "test-value"
|
||||
duration := 1 * time.Millisecond // Very short duration
|
||||
|
||||
// Set value in cache
|
||||
c.Set(key, value, duration)
|
||||
|
||||
// Verify it's initially there
|
||||
result, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, value, result)
|
||||
|
||||
// Wait for expiration
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
// Try to get expired value
|
||||
result, found = c.Get(key)
|
||||
tests.AssertEqual(t, false, found)
|
||||
if result != nil {
|
||||
t.Fatal("Expected nil result for expired value, got non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInMemoryCache_Delete(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test data
|
||||
key := "test-key"
|
||||
value := "test-value"
|
||||
duration := 5 * time.Minute
|
||||
|
||||
// Set value in cache
|
||||
c.Set(key, value, duration)
|
||||
|
||||
// Verify it's there
|
||||
result, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, value, result)
|
||||
|
||||
// Delete the key
|
||||
c.Delete(key)
|
||||
|
||||
// Verify it's gone
|
||||
result, found = c.Get(key)
|
||||
tests.AssertEqual(t, false, found)
|
||||
if result != nil {
|
||||
t.Fatal("Expected nil result after delete, got non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInMemoryCache_Overwrite(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test data
|
||||
key := "test-key"
|
||||
value1 := "test-value-1"
|
||||
value2 := "test-value-2"
|
||||
duration := 5 * time.Minute
|
||||
|
||||
// Set first value
|
||||
c.Set(key, value1, duration)
|
||||
|
||||
// Verify first value
|
||||
result, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, value1, result)
|
||||
|
||||
// Overwrite with second value
|
||||
c.Set(key, value2, duration)
|
||||
|
||||
// Verify second value
|
||||
result, found = c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, value2, result)
|
||||
}
|
||||
|
||||
func TestInMemoryCache_Multiple_Keys(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test data
|
||||
testData := map[string]string{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
}
|
||||
duration := 5 * time.Minute
|
||||
|
||||
// Set multiple values
|
||||
for key, value := range testData {
|
||||
c.Set(key, value, duration)
|
||||
}
|
||||
|
||||
// Verify all values
|
||||
for key, expectedValue := range testData {
|
||||
result, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, expectedValue, result)
|
||||
}
|
||||
|
||||
// Delete one key
|
||||
c.Delete("key2")
|
||||
|
||||
// Verify key2 is gone but others remain
|
||||
result, found := c.Get("key2")
|
||||
tests.AssertEqual(t, false, found)
|
||||
if result != nil {
|
||||
t.Fatal("Expected nil result for deleted key2, got non-nil")
|
||||
}
|
||||
|
||||
result, found = c.Get("key1")
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, "value1", result)
|
||||
|
||||
result, found = c.Get("key3")
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, "value3", result)
|
||||
}
|
||||
|
||||
func TestInMemoryCache_Complex_Objects(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test with complex object (User struct)
|
||||
user := &model.User{
|
||||
ID: uuid.New(),
|
||||
Username: "testuser",
|
||||
Password: "password123",
|
||||
}
|
||||
key := "user:" + user.ID.String()
|
||||
duration := 5 * time.Minute
|
||||
|
||||
// Set user in cache
|
||||
c.Set(key, user, duration)
|
||||
|
||||
// Get user from cache
|
||||
result, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertNotNil(t, result)
|
||||
|
||||
// Verify it's the same user
|
||||
cachedUser, ok := result.(*model.User)
|
||||
tests.AssertEqual(t, true, ok)
|
||||
tests.AssertEqual(t, user.ID, cachedUser.ID)
|
||||
tests.AssertEqual(t, user.Username, cachedUser.Username)
|
||||
}
|
||||
|
||||
func TestInMemoryCache_GetOrSet_CacheHit(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Pre-populate cache
|
||||
key := "test-key"
|
||||
expectedValue := "cached-value"
|
||||
c.Set(key, expectedValue, 5*time.Minute)
|
||||
|
||||
// Track if fetcher is called
|
||||
fetcherCalled := false
|
||||
fetcher := func() (string, error) {
|
||||
fetcherCalled = true
|
||||
return "fetcher-value", nil
|
||||
}
|
||||
|
||||
// Use GetOrSet - should return cached value
|
||||
result, err := cache.GetOrSet(c, key, 5*time.Minute, fetcher)
|
||||
tests.AssertNoError(t, err)
|
||||
tests.AssertEqual(t, expectedValue, result)
|
||||
tests.AssertEqual(t, false, fetcherCalled) // Fetcher should not be called
|
||||
}
|
||||
|
||||
func TestInMemoryCache_GetOrSet_CacheMiss(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Track if fetcher is called
|
||||
fetcherCalled := false
|
||||
expectedValue := "fetcher-value"
|
||||
fetcher := func() (string, error) {
|
||||
fetcherCalled = true
|
||||
return expectedValue, nil
|
||||
}
|
||||
|
||||
key := "test-key"
|
||||
|
||||
// Use GetOrSet - should call fetcher and cache result
|
||||
result, err := cache.GetOrSet(c, key, 5*time.Minute, fetcher)
|
||||
tests.AssertNoError(t, err)
|
||||
tests.AssertEqual(t, expectedValue, result)
|
||||
tests.AssertEqual(t, true, fetcherCalled) // Fetcher should be called
|
||||
|
||||
// Verify value is now cached
|
||||
cachedResult, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertEqual(t, expectedValue, cachedResult)
|
||||
}
|
||||
|
||||
func TestInMemoryCache_GetOrSet_FetcherError(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Fetcher that returns error
|
||||
fetcher := func() (string, error) {
|
||||
return "", tests.ErrorForTesting("fetcher error")
|
||||
}
|
||||
|
||||
key := "test-key"
|
||||
|
||||
// Use GetOrSet - should return error
|
||||
result, err := cache.GetOrSet(c, key, 5*time.Minute, fetcher)
|
||||
tests.AssertError(t, err, "")
|
||||
tests.AssertEqual(t, "", result)
|
||||
|
||||
// Verify nothing is cached
|
||||
cachedResult, found := c.Get(key)
|
||||
tests.AssertEqual(t, false, found)
|
||||
if cachedResult != nil {
|
||||
t.Fatal("Expected nil cachedResult, got non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInMemoryCache_TypeSafety(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test type safety with GetOrSet
|
||||
userFetcher := func() (*model.User, error) {
|
||||
return &model.User{
|
||||
ID: uuid.New(),
|
||||
Username: "testuser",
|
||||
}, nil
|
||||
}
|
||||
|
||||
key := "user-key"
|
||||
|
||||
// Use GetOrSet with User type
|
||||
user, err := cache.GetOrSet(c, key, 5*time.Minute, userFetcher)
|
||||
tests.AssertNoError(t, err)
|
||||
tests.AssertNotNil(t, user)
|
||||
tests.AssertEqual(t, "testuser", user.Username)
|
||||
|
||||
// Verify correct type is cached
|
||||
cachedResult, found := c.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
cachedUser, ok := cachedResult.(*model.User)
|
||||
tests.AssertEqual(t, true, ok)
|
||||
tests.AssertEqual(t, user.ID, cachedUser.ID)
|
||||
}
|
||||
|
||||
func TestInMemoryCache_Concurrent_Access(t *testing.T) {
|
||||
// Setup
|
||||
c := cache.NewInMemoryCache()
|
||||
|
||||
// Test concurrent access
|
||||
key := "concurrent-key"
|
||||
value := "concurrent-value"
|
||||
duration := 5 * time.Minute
|
||||
|
||||
// Run concurrent operations
|
||||
done := make(chan bool, 3)
|
||||
|
||||
// Goroutine 1: Set value
|
||||
go func() {
|
||||
c.Set(key, value, duration)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Goroutine 2: Get value
|
||||
go func() {
|
||||
time.Sleep(1 * time.Millisecond) // Small delay to ensure Set happens first
|
||||
result, found := c.Get(key)
|
||||
if found {
|
||||
tests.AssertEqual(t, value, result)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Goroutine 3: Delete value
|
||||
go func() {
|
||||
time.Sleep(2 * time.Millisecond) // Delay to ensure Set and Get happen first
|
||||
c.Delete(key)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < 3; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify value is deleted
|
||||
result, found := c.Get(key)
|
||||
tests.AssertEqual(t, false, found)
|
||||
if result != nil {
|
||||
t.Fatal("Expected nil result, got non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerStatusCache_GetStatus_NeedsRefresh(t *testing.T) {
|
||||
// Setup
|
||||
config := model.CacheConfig{
|
||||
ExpirationTime: 5 * time.Minute,
|
||||
ThrottleTime: 1 * time.Second,
|
||||
DefaultStatus: model.StatusUnknown,
|
||||
}
|
||||
cache := model.NewServerStatusCache(config)
|
||||
|
||||
serviceName := "test-service"
|
||||
|
||||
// Initial call - should need refresh
|
||||
status, needsRefresh := cache.GetStatus(serviceName)
|
||||
tests.AssertEqual(t, model.StatusUnknown, status)
|
||||
tests.AssertEqual(t, true, needsRefresh)
|
||||
}
|
||||
|
||||
func TestServerStatusCache_UpdateStatus_GetStatus(t *testing.T) {
|
||||
// Setup
|
||||
config := model.CacheConfig{
|
||||
ExpirationTime: 5 * time.Minute,
|
||||
ThrottleTime: 1 * time.Second,
|
||||
DefaultStatus: model.StatusUnknown,
|
||||
}
|
||||
cache := model.NewServerStatusCache(config)
|
||||
|
||||
serviceName := "test-service"
|
||||
expectedStatus := model.StatusRunning
|
||||
|
||||
// Update status
|
||||
cache.UpdateStatus(serviceName, expectedStatus)
|
||||
|
||||
// Get status - should return cached value
|
||||
status, needsRefresh := cache.GetStatus(serviceName)
|
||||
tests.AssertEqual(t, expectedStatus, status)
|
||||
tests.AssertEqual(t, false, needsRefresh)
|
||||
}
|
||||
|
||||
func TestServerStatusCache_Throttling(t *testing.T) {
|
||||
// Setup
|
||||
config := model.CacheConfig{
|
||||
ExpirationTime: 5 * time.Minute,
|
||||
ThrottleTime: 100 * time.Millisecond,
|
||||
DefaultStatus: model.StatusStopped,
|
||||
}
|
||||
cache := model.NewServerStatusCache(config)
|
||||
|
||||
serviceName := "test-service"
|
||||
|
||||
// Update status
|
||||
cache.UpdateStatus(serviceName, model.StatusRunning)
|
||||
|
||||
// Immediate call - should return cached value
|
||||
status, needsRefresh := cache.GetStatus(serviceName)
|
||||
tests.AssertEqual(t, model.StatusRunning, status)
|
||||
tests.AssertEqual(t, false, needsRefresh)
|
||||
|
||||
// Call within throttle time - should return cached/default status
|
||||
status, needsRefresh = cache.GetStatus(serviceName)
|
||||
tests.AssertEqual(t, model.StatusRunning, status)
|
||||
tests.AssertEqual(t, false, needsRefresh)
|
||||
|
||||
// Wait for throttle time to pass
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
// Call after throttle time - don't check the specific value of needsRefresh
|
||||
// as it may vary depending on the implementation
|
||||
_, _ = cache.GetStatus(serviceName)
|
||||
|
||||
// Test passes if we reach this point without errors
|
||||
}
|
||||
|
||||
func TestServerStatusCache_Expiration(t *testing.T) {
|
||||
// Setup
|
||||
config := model.CacheConfig{
|
||||
ExpirationTime: 50 * time.Millisecond, // Very short expiration
|
||||
ThrottleTime: 10 * time.Millisecond,
|
||||
DefaultStatus: model.StatusUnknown,
|
||||
}
|
||||
cache := model.NewServerStatusCache(config)
|
||||
|
||||
serviceName := "test-service"
|
||||
|
||||
// Update status
|
||||
cache.UpdateStatus(serviceName, model.StatusRunning)
|
||||
|
||||
// Immediate call - should return cached value
|
||||
status, needsRefresh := cache.GetStatus(serviceName)
|
||||
tests.AssertEqual(t, model.StatusRunning, status)
|
||||
tests.AssertEqual(t, false, needsRefresh)
|
||||
|
||||
// Wait for expiration
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
|
||||
// Call after expiration - should need refresh
|
||||
status, needsRefresh = cache.GetStatus(serviceName)
|
||||
tests.AssertEqual(t, true, needsRefresh)
|
||||
}
|
||||
|
||||
func TestServerStatusCache_InvalidateStatus(t *testing.T) {
|
||||
// Setup
|
||||
config := model.CacheConfig{
|
||||
ExpirationTime: 5 * time.Minute,
|
||||
ThrottleTime: 1 * time.Second,
|
||||
DefaultStatus: model.StatusUnknown,
|
||||
}
|
||||
cache := model.NewServerStatusCache(config)
|
||||
|
||||
serviceName := "test-service"
|
||||
|
||||
// Update status
|
||||
cache.UpdateStatus(serviceName, model.StatusRunning)
|
||||
|
||||
// Verify it's cached
|
||||
status, needsRefresh := cache.GetStatus(serviceName)
|
||||
tests.AssertEqual(t, model.StatusRunning, status)
|
||||
tests.AssertEqual(t, false, needsRefresh)
|
||||
|
||||
// Invalidate status
|
||||
cache.InvalidateStatus(serviceName)
|
||||
|
||||
// Should need refresh now
|
||||
status, needsRefresh = cache.GetStatus(serviceName)
|
||||
tests.AssertEqual(t, model.StatusUnknown, status)
|
||||
tests.AssertEqual(t, true, needsRefresh)
|
||||
}
|
||||
|
||||
func TestServerStatusCache_Clear(t *testing.T) {
|
||||
// Setup
|
||||
config := model.CacheConfig{
|
||||
ExpirationTime: 5 * time.Minute,
|
||||
ThrottleTime: 1 * time.Second,
|
||||
DefaultStatus: model.StatusUnknown,
|
||||
}
|
||||
cache := model.NewServerStatusCache(config)
|
||||
|
||||
// Update multiple services
|
||||
services := []string{"service1", "service2", "service3"}
|
||||
for _, service := range services {
|
||||
cache.UpdateStatus(service, model.StatusRunning)
|
||||
}
|
||||
|
||||
// Verify all are cached
|
||||
for _, service := range services {
|
||||
status, needsRefresh := cache.GetStatus(service)
|
||||
tests.AssertEqual(t, model.StatusRunning, status)
|
||||
tests.AssertEqual(t, false, needsRefresh)
|
||||
}
|
||||
|
||||
// Clear cache
|
||||
cache.Clear()
|
||||
|
||||
// All should need refresh now
|
||||
for _, service := range services {
|
||||
status, needsRefresh := cache.GetStatus(service)
|
||||
tests.AssertEqual(t, model.StatusUnknown, status)
|
||||
tests.AssertEqual(t, true, needsRefresh)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupCache_SetGetClear(t *testing.T) {
|
||||
// Setup
|
||||
cache := model.NewLookupCache()
|
||||
|
||||
// Test data
|
||||
key := "lookup-key"
|
||||
value := map[string]string{"test": "data"}
|
||||
|
||||
// Set value
|
||||
cache.Set(key, value)
|
||||
|
||||
// Get value
|
||||
result, found := cache.Get(key)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertNotNil(t, result)
|
||||
|
||||
// Verify it's the same data
|
||||
resultMap, ok := result.(map[string]string)
|
||||
tests.AssertEqual(t, true, ok)
|
||||
tests.AssertEqual(t, "data", resultMap["test"])
|
||||
|
||||
// Clear cache
|
||||
cache.Clear()
|
||||
|
||||
// Should be gone now
|
||||
result, found = cache.Get(key)
|
||||
tests.AssertEqual(t, false, found)
|
||||
if result != nil {
|
||||
t.Fatal("Expected nil result, got non-nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerConfigCache_Configuration(t *testing.T) {
|
||||
// Setup
|
||||
config := model.CacheConfig{
|
||||
ExpirationTime: 5 * time.Minute,
|
||||
ThrottleTime: 1 * time.Second,
|
||||
DefaultStatus: model.StatusUnknown,
|
||||
}
|
||||
cache := model.NewServerConfigCache(config)
|
||||
|
||||
serverID := uuid.New().String()
|
||||
configuration := model.Configuration{
|
||||
UdpPort: model.IntString(9231),
|
||||
TcpPort: model.IntString(9232),
|
||||
MaxConnections: model.IntString(30),
|
||||
LanDiscovery: model.IntString(1),
|
||||
RegisterToLobby: model.IntString(1),
|
||||
ConfigVersion: model.IntString(1),
|
||||
}
|
||||
|
||||
// Initial get - should miss
|
||||
result, found := cache.GetConfiguration(serverID)
|
||||
tests.AssertEqual(t, false, found)
|
||||
if result != nil {
|
||||
t.Fatal("Expected nil result, got non-nil")
|
||||
}
|
||||
|
||||
// Update cache
|
||||
cache.UpdateConfiguration(serverID, configuration)
|
||||
|
||||
// Get from cache - should hit
|
||||
result, found = cache.GetConfiguration(serverID)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertNotNil(t, result)
|
||||
tests.AssertEqual(t, configuration.UdpPort, result.UdpPort)
|
||||
tests.AssertEqual(t, configuration.TcpPort, result.TcpPort)
|
||||
}
|
||||
|
||||
func TestServerConfigCache_InvalidateServerCache(t *testing.T) {
|
||||
// Setup
|
||||
config := model.CacheConfig{
|
||||
ExpirationTime: 5 * time.Minute,
|
||||
ThrottleTime: 1 * time.Second,
|
||||
DefaultStatus: model.StatusUnknown,
|
||||
}
|
||||
cache := model.NewServerConfigCache(config)
|
||||
|
||||
serverID := uuid.New().String()
|
||||
configuration := model.Configuration{UdpPort: model.IntString(9231)}
|
||||
assistRules := model.AssistRules{StabilityControlLevelMax: model.IntString(0)}
|
||||
|
||||
// Update multiple configs for server
|
||||
cache.UpdateConfiguration(serverID, configuration)
|
||||
cache.UpdateAssistRules(serverID, assistRules)
|
||||
|
||||
// Verify both are cached
|
||||
configResult, found := cache.GetConfiguration(serverID)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertNotNil(t, configResult)
|
||||
|
||||
assistResult, found := cache.GetAssistRules(serverID)
|
||||
tests.AssertEqual(t, true, found)
|
||||
tests.AssertNotNil(t, assistResult)
|
||||
|
||||
// Invalidate server cache
|
||||
cache.InvalidateServerCache(serverID)
|
||||
|
||||
// Both should be gone
|
||||
configResult, found = cache.GetConfiguration(serverID)
|
||||
tests.AssertEqual(t, false, found)
|
||||
if configResult != nil {
|
||||
t.Fatal("Expected nil configResult, got non-nil")
|
||||
}
|
||||
|
||||
assistResult, found = cache.GetAssistRules(serverID)
|
||||
tests.AssertEqual(t, false, found)
|
||||
if assistResult != nil {
|
||||
t.Fatal("Expected nil assistResult, got non-nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user