mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-30 04:20:20 +00:00
feat(auth): add policy evaluator and SQLite auth manager for RBAC
- Add policy evaluator for fine-grained access control - Implement SQLite-backed auth manager for user/role persistence - Support role-based permissions evaluation
This commit is contained in:
parent
d0ba203203
commit
0ddbf37c59
4 changed files with 2172 additions and 0 deletions
579
pkg/auth/sqlite_manager_test.go
Normal file
579
pkg/auth/sqlite_manager_test.go
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSQLiteManager(t *testing.T) {
|
||||
// Create temp directory for tests
|
||||
tmpDir, err := os.MkdirTemp("", "rbac-sqlite-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
m, err := NewSQLiteManager(SQLiteManagerConfig{
|
||||
DataDir: tmpDir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SQLiteManager: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
t.Run("Built-in roles exist", func(t *testing.T) {
|
||||
roles := m.GetRoles()
|
||||
if len(roles) < 4 {
|
||||
t.Errorf("Expected at least 4 built-in roles, got %d", len(roles))
|
||||
}
|
||||
|
||||
// Check admin role exists
|
||||
admin, ok := m.GetRole(RoleAdmin)
|
||||
if !ok {
|
||||
t.Error("Admin role not found")
|
||||
}
|
||||
if !admin.IsBuiltIn {
|
||||
t.Error("Admin role should be built-in")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cannot delete built-in role", func(t *testing.T) {
|
||||
err := m.DeleteRole(RoleAdmin)
|
||||
if err == nil {
|
||||
t.Error("Expected error when deleting built-in role")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Cannot modify built-in role", func(t *testing.T) {
|
||||
admin, _ := m.GetRole(RoleAdmin)
|
||||
admin.Name = "Modified Admin"
|
||||
err := m.SaveRole(admin)
|
||||
if err == nil {
|
||||
t.Error("Expected error when modifying built-in role")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create custom role with conditions", func(t *testing.T) {
|
||||
customRole := Role{
|
||||
ID: "custom-abac",
|
||||
Name: "Custom ABAC Role",
|
||||
Description: "A custom role with ABAC conditions",
|
||||
Permissions: []Permission{
|
||||
{Action: "read", Resource: "nodes", Effect: EffectAllow},
|
||||
{Action: "write", Resource: "nodes:production", Effect: EffectDeny},
|
||||
{
|
||||
Action: "read",
|
||||
Resource: "nodes:*",
|
||||
Effect: EffectAllow,
|
||||
Conditions: map[string]string{"tag": "test", "owner": "${user}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := m.SaveRole(customRole); err != nil {
|
||||
t.Errorf("Failed to save custom role: %v", err)
|
||||
}
|
||||
|
||||
retrieved, ok := m.GetRole("custom-abac")
|
||||
if !ok {
|
||||
t.Error("Custom role not found after save")
|
||||
}
|
||||
if retrieved.Name != "Custom ABAC Role" {
|
||||
t.Errorf("Expected name 'Custom ABAC Role', got '%s'", retrieved.Name)
|
||||
}
|
||||
if len(retrieved.Permissions) != 3 {
|
||||
t.Errorf("Expected 3 permissions, got %d", len(retrieved.Permissions))
|
||||
}
|
||||
|
||||
// Check permission with conditions
|
||||
var foundCondPerm bool
|
||||
for _, p := range retrieved.Permissions {
|
||||
if p.Conditions != nil && p.Conditions["tag"] == "test" {
|
||||
foundCondPerm = true
|
||||
if p.Conditions["owner"] != "${user}" {
|
||||
t.Errorf("Expected owner condition '${user}', got '%s'", p.Conditions["owner"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundCondPerm {
|
||||
t.Error("Permission with conditions not found")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create role with inheritance", func(t *testing.T) {
|
||||
// First create parent role
|
||||
parentRole := Role{
|
||||
ID: "parent-role",
|
||||
Name: "Parent Role",
|
||||
Description: "A parent role",
|
||||
Permissions: []Permission{
|
||||
{Action: "read", Resource: "settings"},
|
||||
},
|
||||
}
|
||||
if err := m.SaveRole(parentRole); err != nil {
|
||||
t.Errorf("Failed to save parent role: %v", err)
|
||||
}
|
||||
|
||||
// Create child role
|
||||
childRole := Role{
|
||||
ID: "child-role",
|
||||
Name: "Child Role",
|
||||
Description: "A child role inheriting from parent",
|
||||
ParentID: "parent-role",
|
||||
Permissions: []Permission{
|
||||
{Action: "write", Resource: "settings"},
|
||||
},
|
||||
}
|
||||
if err := m.SaveRole(childRole); err != nil {
|
||||
t.Errorf("Failed to save child role: %v", err)
|
||||
}
|
||||
|
||||
// Get child with inheritance
|
||||
role, effectivePerms, ok := m.GetRoleWithInheritance("child-role")
|
||||
if !ok {
|
||||
t.Error("Child role not found")
|
||||
}
|
||||
if role.ParentID != "parent-role" {
|
||||
t.Errorf("Expected parent ID 'parent-role', got '%s'", role.ParentID)
|
||||
}
|
||||
|
||||
// Should have both own permissions and inherited permissions
|
||||
if len(effectivePerms) < 2 {
|
||||
t.Errorf("Expected at least 2 effective permissions, got %d", len(effectivePerms))
|
||||
}
|
||||
|
||||
var hasRead, hasWrite bool
|
||||
for _, p := range effectivePerms {
|
||||
if p.Action == "read" && p.Resource == "settings" {
|
||||
hasRead = true
|
||||
}
|
||||
if p.Action == "write" && p.Resource == "settings" {
|
||||
hasWrite = true
|
||||
}
|
||||
}
|
||||
if !hasRead {
|
||||
t.Error("Missing inherited read permission")
|
||||
}
|
||||
if !hasWrite {
|
||||
t.Error("Missing own write permission")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Assign roles to user", func(t *testing.T) {
|
||||
if err := m.AssignRole("testuser", RoleViewer); err != nil {
|
||||
t.Errorf("Failed to assign role: %v", err)
|
||||
}
|
||||
if err := m.AssignRole("testuser", "child-role"); err != nil {
|
||||
t.Errorf("Failed to assign child role: %v", err)
|
||||
}
|
||||
|
||||
assignment, ok := m.GetUserAssignment("testuser")
|
||||
if !ok {
|
||||
t.Error("User assignment not found")
|
||||
}
|
||||
if len(assignment.RoleIDs) != 2 {
|
||||
t.Errorf("Expected 2 roles, got %d", len(assignment.RoleIDs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get roles with inheritance for user", func(t *testing.T) {
|
||||
roles := m.GetRolesWithInheritance("testuser")
|
||||
// Should include viewer, child-role, and parent-role (inherited)
|
||||
if len(roles) < 3 {
|
||||
t.Errorf("Expected at least 3 roles with inheritance, got %d", len(roles))
|
||||
}
|
||||
|
||||
var hasParent bool
|
||||
for _, r := range roles {
|
||||
if r.ID == "parent-role" {
|
||||
hasParent = true
|
||||
}
|
||||
}
|
||||
if !hasParent {
|
||||
t.Error("Parent role should be included via inheritance")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get user permissions", func(t *testing.T) {
|
||||
perms := m.GetUserPermissions("testuser")
|
||||
if len(perms) == 0 {
|
||||
t.Error("Expected permissions for user")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete custom role", func(t *testing.T) {
|
||||
if err := m.DeleteRole("custom-abac"); err != nil {
|
||||
t.Errorf("Failed to delete custom role: %v", err)
|
||||
}
|
||||
|
||||
_, ok := m.GetRole("custom-abac")
|
||||
if ok {
|
||||
t.Error("Custom role should not exist after delete")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Changelog is recorded", func(t *testing.T) {
|
||||
logs := m.GetChangeLogs(100, 0)
|
||||
if len(logs) == 0 {
|
||||
t.Error("Expected changelog entries")
|
||||
}
|
||||
|
||||
// Should have entries for role creation
|
||||
var hasRoleCreated bool
|
||||
for _, l := range logs {
|
||||
if l.Action == ActionRoleCreated {
|
||||
hasRoleCreated = true
|
||||
}
|
||||
}
|
||||
if !hasRoleCreated {
|
||||
t.Error("Missing role_created changelog entry")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Get changelog for entity", func(t *testing.T) {
|
||||
logs := m.GetChangeLogsForEntity("role", "parent-role")
|
||||
if len(logs) == 0 {
|
||||
t.Error("Expected changelog entries for parent-role")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSQLiteManagerMigration(t *testing.T) {
|
||||
// Create temp directory for tests
|
||||
tmpDir, err := os.MkdirTemp("", "rbac-migration-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// First create file-based manager with some data
|
||||
fileManager, err := NewFileManager(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create FileManager: %v", err)
|
||||
}
|
||||
|
||||
// Add custom role and assignment
|
||||
customRole := Role{
|
||||
ID: "migrate-test",
|
||||
Name: "Migration Test Role",
|
||||
Description: "Should be migrated",
|
||||
Permissions: []Permission{{Action: "read", Resource: "alerts"}},
|
||||
}
|
||||
if err := fileManager.SaveRole(customRole); err != nil {
|
||||
t.Fatalf("Failed to save role in FileManager: %v", err)
|
||||
}
|
||||
if err := fileManager.AssignRole("migrateuser", "migrate-test"); err != nil {
|
||||
t.Fatalf("Failed to assign role in FileManager: %v", err)
|
||||
}
|
||||
|
||||
// Now create SQLite manager with migration enabled
|
||||
sqliteManager, err := NewSQLiteManager(SQLiteManagerConfig{
|
||||
DataDir: tmpDir,
|
||||
MigrateFromFiles: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SQLiteManager: %v", err)
|
||||
}
|
||||
defer sqliteManager.Close()
|
||||
|
||||
// Check custom role was migrated
|
||||
t.Run("Custom role migrated", func(t *testing.T) {
|
||||
role, ok := sqliteManager.GetRole("migrate-test")
|
||||
if !ok {
|
||||
t.Error("Custom role should be migrated to SQLite")
|
||||
}
|
||||
if role.Name != "Migration Test Role" {
|
||||
t.Errorf("Expected 'Migration Test Role', got '%s'", role.Name)
|
||||
}
|
||||
})
|
||||
|
||||
// Check assignment was migrated
|
||||
t.Run("User assignment migrated", func(t *testing.T) {
|
||||
assignment, ok := sqliteManager.GetUserAssignment("migrateuser")
|
||||
if !ok {
|
||||
t.Error("User assignment should be migrated to SQLite")
|
||||
}
|
||||
hasRole := false
|
||||
for _, rid := range assignment.RoleIDs {
|
||||
if rid == "migrate-test" {
|
||||
hasRole = true
|
||||
}
|
||||
}
|
||||
if !hasRole {
|
||||
t.Error("User should have migrated role")
|
||||
}
|
||||
})
|
||||
|
||||
// Check backup files created
|
||||
t.Run("Backup files created", func(t *testing.T) {
|
||||
rolesBackup := filepath.Join(tmpDir, "rbac_roles.json.bak")
|
||||
if _, err := os.Stat(rolesBackup); os.IsNotExist(err) {
|
||||
t.Error("Roles backup file should be created")
|
||||
}
|
||||
assignmentsBackup := filepath.Join(tmpDir, "rbac_assignments.json.bak")
|
||||
if _, err := os.Stat(assignmentsBackup); os.IsNotExist(err) {
|
||||
t.Error("Assignments backup file should be created")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSQLiteManagerCircularInheritance(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "rbac-circular-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SQLiteManager: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
// Create role A
|
||||
roleA := Role{ID: "role-a", Name: "Role A", Permissions: []Permission{{Action: "read", Resource: "a"}}}
|
||||
m.SaveRole(roleA)
|
||||
|
||||
// Create role B with parent A
|
||||
roleB := Role{ID: "role-b", Name: "Role B", ParentID: "role-a", Permissions: []Permission{{Action: "read", Resource: "b"}}}
|
||||
m.SaveRole(roleB)
|
||||
|
||||
// Create role C with parent B
|
||||
roleC := Role{ID: "role-c", Name: "Role C", ParentID: "role-b", Permissions: []Permission{{Action: "read", Resource: "c"}}}
|
||||
m.SaveRole(roleC)
|
||||
|
||||
// Try to make A inherit from C (creating cycle)
|
||||
roleA.ParentID = "role-c"
|
||||
err = m.SaveRole(roleA)
|
||||
if err == nil {
|
||||
t.Error("Expected error when creating circular inheritance")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteManagerContextOperations(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "rbac-context-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SQLiteManager: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
t.Run("SaveRoleWithContext records user", func(t *testing.T) {
|
||||
role := Role{
|
||||
ID: "context-role",
|
||||
Name: "Context Test Role",
|
||||
Permissions: []Permission{{Action: "read", Resource: "test"}},
|
||||
}
|
||||
if err := m.SaveRoleWithContext(role, "admin-user"); err != nil {
|
||||
t.Errorf("Failed to save role with context: %v", err)
|
||||
}
|
||||
|
||||
// Check changelog has user
|
||||
logs := m.GetChangeLogsForEntity("role", "context-role")
|
||||
if len(logs) == 0 {
|
||||
t.Fatal("Expected changelog entry")
|
||||
}
|
||||
if logs[0].User != "admin-user" {
|
||||
t.Errorf("Expected user 'admin-user', got '%s'", logs[0].User)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteRoleWithContext records user", func(t *testing.T) {
|
||||
if err := m.DeleteRoleWithContext("context-role", "admin-user"); err != nil {
|
||||
t.Errorf("Failed to delete role with context: %v", err)
|
||||
}
|
||||
|
||||
logs := m.GetChangeLogsForEntity("role", "context-role")
|
||||
var hasDelete bool
|
||||
for _, l := range logs {
|
||||
if l.Action == ActionRoleDeleted && l.User == "admin-user" {
|
||||
hasDelete = true
|
||||
}
|
||||
}
|
||||
if !hasDelete {
|
||||
t.Error("Missing delete changelog with user")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("UpdateUserRolesWithContext records user", func(t *testing.T) {
|
||||
if err := m.UpdateUserRolesWithContext("context-user", []string{RoleViewer}, "admin-user"); err != nil {
|
||||
t.Errorf("Failed to update user roles with context: %v", err)
|
||||
}
|
||||
|
||||
logs := m.GetChangeLogsForEntity("assignment", "context-user")
|
||||
if len(logs) == 0 {
|
||||
t.Fatal("Expected changelog entry")
|
||||
}
|
||||
if logs[0].User != "admin-user" {
|
||||
t.Errorf("Expected user 'admin-user', got '%s'", logs[0].User)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSQLiteManagerPersistence(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "rbac-persist-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
// Create manager and add data
|
||||
m1, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SQLiteManager: %v", err)
|
||||
}
|
||||
|
||||
role := Role{
|
||||
ID: "persist-role",
|
||||
Name: "Persist Test",
|
||||
ParentID: RoleViewer,
|
||||
Permissions: []Permission{{Action: "write", Resource: "persist", Effect: EffectAllow}},
|
||||
}
|
||||
m1.SaveRole(role)
|
||||
m1.AssignRole("persist-user", "persist-role")
|
||||
m1.Close()
|
||||
|
||||
// Reopen and verify
|
||||
m2, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to reopen SQLiteManager: %v", err)
|
||||
}
|
||||
defer m2.Close()
|
||||
|
||||
t.Run("Role persisted", func(t *testing.T) {
|
||||
r, ok := m2.GetRole("persist-role")
|
||||
if !ok {
|
||||
t.Error("Role should persist")
|
||||
}
|
||||
if r.ParentID != RoleViewer {
|
||||
t.Errorf("Expected parent %s, got %s", RoleViewer, r.ParentID)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Assignment persisted", func(t *testing.T) {
|
||||
a, ok := m2.GetUserAssignment("persist-user")
|
||||
if !ok {
|
||||
t.Error("Assignment should persist")
|
||||
}
|
||||
if len(a.RoleIDs) != 1 {
|
||||
t.Errorf("Expected 1 role, got %d", len(a.RoleIDs))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Changelog persisted", func(t *testing.T) {
|
||||
logs := m2.GetChangeLogs(100, 0)
|
||||
if len(logs) == 0 {
|
||||
t.Error("Changelog should persist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSQLiteManagerDenyPermission(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "rbac-deny-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SQLiteManager: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
// Create role with both allow and deny permissions
|
||||
role := Role{
|
||||
ID: "deny-test",
|
||||
Name: "Deny Test Role",
|
||||
Permissions: []Permission{
|
||||
{Action: "read", Resource: "*", Effect: EffectAllow}, // Allow all reads
|
||||
{Action: "read", Resource: "secrets", Effect: EffectDeny}, // But deny reading secrets
|
||||
{Action: "write", Resource: "settings", Effect: EffectAllow}, // Allow writing settings
|
||||
{Action: "write", Resource: "settings:admin", Effect: EffectDeny}, // But deny admin settings
|
||||
},
|
||||
}
|
||||
if err := m.SaveRole(role); err != nil {
|
||||
t.Fatalf("Failed to save role: %v", err)
|
||||
}
|
||||
|
||||
retrieved, ok := m.GetRole("deny-test")
|
||||
if !ok {
|
||||
t.Fatal("Role not found")
|
||||
}
|
||||
|
||||
// Verify deny effects are preserved
|
||||
var denyCount, allowCount int
|
||||
for _, p := range retrieved.Permissions {
|
||||
if p.GetEffect() == EffectDeny {
|
||||
denyCount++
|
||||
} else {
|
||||
allowCount++
|
||||
}
|
||||
}
|
||||
if denyCount != 2 {
|
||||
t.Errorf("Expected 2 deny permissions, got %d", denyCount)
|
||||
}
|
||||
if allowCount != 2 {
|
||||
t.Errorf("Expected 2 allow permissions, got %d", allowCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLiteManagerChangeLogRetention(t *testing.T) {
|
||||
tmpDir, err := os.MkdirTemp("", "rbac-retention-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
m, err := NewSQLiteManager(SQLiteManagerConfig{DataDir: tmpDir})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create SQLiteManager: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
// Create several roles to generate changelog entries
|
||||
for i := 0; i < 5; i++ {
|
||||
role := Role{
|
||||
ID: "retention-role-" + string(rune('a'+i)),
|
||||
Name: "Retention Test " + string(rune('a'+i)),
|
||||
Permissions: []Permission{{Action: "read", Resource: "test"}},
|
||||
}
|
||||
m.SaveRole(role)
|
||||
// Add slight delay to ensure different timestamps
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
t.Run("Pagination works", func(t *testing.T) {
|
||||
// Get first 2
|
||||
logs1 := m.GetChangeLogs(2, 0)
|
||||
if len(logs1) != 2 {
|
||||
t.Errorf("Expected 2 logs, got %d", len(logs1))
|
||||
}
|
||||
|
||||
// Get next 2
|
||||
logs2 := m.GetChangeLogs(2, 2)
|
||||
if len(logs2) != 2 {
|
||||
t.Errorf("Expected 2 logs, got %d", len(logs2))
|
||||
}
|
||||
|
||||
// Ensure they're different
|
||||
if len(logs1) > 0 && len(logs2) > 0 && logs1[0].ID == logs2[0].ID {
|
||||
t.Error("Pagination returned same entries")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Logs ordered by timestamp desc", func(t *testing.T) {
|
||||
logs := m.GetChangeLogs(100, 0)
|
||||
for i := 1; i < len(logs); i++ {
|
||||
if logs[i].Timestamp.After(logs[i-1].Timestamp) {
|
||||
t.Error("Logs should be ordered by timestamp descending")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue