diff --git a/node-registrar/pkg/db/accounts.go b/node-registrar/pkg/db/accounts.go index c78dba7..302ce58 100644 --- a/node-registrar/pkg/db/accounts.go +++ b/node-registrar/pkg/db/accounts.go @@ -2,6 +2,7 @@ package db import ( "errors" + "strings" "github.com/lib/pq" "gorm.io/gorm" @@ -9,6 +10,10 @@ import ( // CreateAccount creates a new account in the database func (db *Database) CreateAccount(account *Account) error { + if strings.TrimSpace(account.PublicKey) == "" { + return errors.New("public key cannot be empty") + } + err := db.gormDB.Create(account).Error if errors.Is(err, gorm.ErrDuplicatedKey) { return ErrRecordAlreadyExists diff --git a/node-registrar/pkg/db/accounts_test.go b/node-registrar/pkg/db/accounts_test.go new file mode 100644 index 0000000..75bb8eb --- /dev/null +++ b/node-registrar/pkg/db/accounts_test.go @@ -0,0 +1,148 @@ +package db + +import ( + "testing" + + "github.com/lib/pq" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateAccount(t *testing.T) { + BeforeEach(t) + + t.Run("Create Account", func(t *testing.T) { + account := Account{ + PublicKey: "test_public_key_new_1", + Relays: pq.StringArray{"relay4.example.com"}, + RMBEncKey: "test_rmb_key_4", + } + + err := testDB.CreateAccount(&account) + require.NoError(t, err) + require.NotZero(t, account.TwinID) + require.NotZero(t, account.CreatedAt) + require.NotZero(t, account.UpdatedAt) + + createdAccount, err := testDB.GetAccount(account.TwinID) + require.NoError(t, err) + require.Equal(t, account.PublicKey, createdAccount.PublicKey) + require.Equal(t, account.Relays, createdAccount.Relays) + require.Equal(t, account.RMBEncKey, createdAccount.RMBEncKey) + }) + + t.Run("Create Account with Empty PublicKey", func(t *testing.T) { + account := Account{ + PublicKey: "", + Relays: pq.StringArray{"relay5.example.com"}, + RMBEncKey: "test_rmb_key_5", + } + + err := testDB.CreateAccount(&account) + require.Error(t, err) + require.Contains(t, err.Error(), "public key cannot be empty") + }) + + t.Run("Create Account with Missing Optional Fields", func(t *testing.T) { + account := Account{ + PublicKey: "test_public_key_optional", + } + + err := testDB.CreateAccount(&account) + require.NoError(t, err) + require.NotZero(t, account.TwinID) + + createdAccount, err := testDB.GetAccount(account.TwinID) + require.NoError(t, err) + require.Equal(t, account.PublicKey, createdAccount.PublicKey) + require.Equal(t, pq.StringArray{}, createdAccount.Relays) + require.Equal(t, "", createdAccount.RMBEncKey) + }) + + t.Run("Create Duplicate Account", func(t *testing.T) { + duplicatePublicKey := "test_public_key_duplicate" + + account1 := Account{ + PublicKey: duplicatePublicKey, + } + + err := testDB.CreateAccount(&account1) + require.NoError(t, err) + require.NotZero(t, account1.TwinID) + + createdAccount, err := testDB.GetAccount(account1.TwinID) + require.NoError(t, err) + require.Equal(t, duplicatePublicKey, createdAccount.PublicKey) + + account2 := Account{ + PublicKey: duplicatePublicKey, + } + + err = testDB.CreateAccount(&account2) + require.Error(t, err) + }) +} + +func TestGetAccount(t *testing.T) { + BeforeEach(t) + + t.Run("Get Existing Account", func(t *testing.T) { + account, err := testDB.GetAccount(1) + assert.NoError(t, err) + assert.Equal(t, uint64(1), account.TwinID) + assert.Equal(t, "test_public_key_1", account.PublicKey) + assert.Equal(t, pq.StringArray{"relay1.example.com", "relay2.example.com"}, account.Relays) + assert.Equal(t, "test_rmb_key_1", account.RMBEncKey) + }) + + t.Run("Get Non-existent Account", func(t *testing.T) { + account, err := testDB.GetAccount(999) + assert.Error(t, err) + assert.Equal(t, ErrRecordNotFound, err) + assert.Equal(t, Account{}, account) + }) +} + +func TestGetAccountByPublicKey(t *testing.T) { + BeforeEach(t) + + t.Run("Get Account by Existing Public Key", func(t *testing.T) { + account, err := testDB.GetAccountByPublicKey("test_public_key_2") + assert.NoError(t, err) + assert.Equal(t, uint64(2), account.TwinID) + assert.Equal(t, "test_public_key_2", account.PublicKey) + }) + + t.Run("Get Account by Non-existent Public Key", func(t *testing.T) { + account, err := testDB.GetAccountByPublicKey("non_existent_key") + assert.Error(t, err) + assert.Equal(t, ErrRecordNotFound, err) + assert.Equal(t, Account{}, account) + }) +} + +func TestUpdateAccount(t *testing.T) { + BeforeEach(t) + + t.Run("Update Existing Account", func(t *testing.T) { + newRelays := pq.StringArray{"updated_relay1.com", "updated_relay2.com"} + newRMBEncKey := "updated_rmb_key" + + err := testDB.UpdateAccount(1, newRelays, newRMBEncKey) + assert.NoError(t, err) + + account, err := testDB.GetAccount(1) + assert.NoError(t, err) + assert.Equal(t, newRelays, account.Relays) + assert.Equal(t, newRMBEncKey, account.RMBEncKey) + }) + + t.Run("Update Non-existent Account", func(t *testing.T) { + newRelays := pq.StringArray{"some_relay.com"} + newRMBEncKey := "some_key" + + err := testDB.UpdateAccount(999, newRelays, newRMBEncKey) + assert.Error(t, err) + assert.Equal(t, ErrRecordNotFound, err) + }) +} diff --git a/node-registrar/pkg/db/db_test.go b/node-registrar/pkg/db/db_test.go new file mode 100644 index 0000000..b29b76f --- /dev/null +++ b/node-registrar/pkg/db/db_test.go @@ -0,0 +1,300 @@ +package db + +import ( + "log" + "os" + "strings" + "testing" + "time" + + "github.com/lib/pq" + "github.com/stretchr/testify/require" + "gorm.io/gorm/logger" +) + +var testDB Database +var testData *TestFixtures + +type TestFixtures struct { + Accounts []Account + Farms []Farm + Nodes []Node + UptimeReports []UptimeReport + ZosVersions []ZosVersion +} + +func TestMain(m *testing.M) { + + if err := setupTestDB(); err != nil { + log.Printf("Failed to setup test database: %v", err) + os.Exit(1) + } + + testData = createTestFixtures() + + code := m.Run() + + if testDB.gormDB != nil { + testDB.Close() + } + os.Exit(code) +} + +func setupTestDB() error { + testConfig := Config{ + PostgresHost: "localhost", + PostgresPort: 5432, + DBName: "test_noderegistrar", + PostgresUser: "postgres", + PostgresPassword: "postgres", + SSLMode: "disable", + SqlLogLevel: logger.Silent, + MaxOpenConns: 5, + MaxIdleConns: 2, + } + + db, err := NewDB(testConfig) + if err != nil { + log.Printf("Cannot connect to test database, skipping tests: %v", err) + return nil + } + + testDB = db + return nil +} + +func BeforeEach(t *testing.T) { + if testDB.gormDB == nil { + t.Skip("Test database not available") + } + + cleanDatabase() + + loadTestData(t) +} + +func cleanDatabase() { + if testDB.gormDB == nil { + return + } + + testDB.gormDB.Exec("DELETE FROM uptime_reports") + testDB.gormDB.Exec("DELETE FROM nodes") + testDB.gormDB.Exec("DELETE FROM farms") + testDB.gormDB.Exec("DELETE FROM accounts") + testDB.gormDB.Exec("DELETE FROM zos_versions") + + // Reset sequences to avoid primary key conflicts + testDB.gormDB.Exec("ALTER SEQUENCE accounts_twin_id_seq RESTART WITH 1") + testDB.gormDB.Exec("ALTER SEQUENCE farms_farm_id_seq RESTART WITH 1") + testDB.gormDB.Exec("ALTER SEQUENCE nodes_node_id_seq RESTART WITH 1") + testDB.gormDB.Exec("ALTER SEQUENCE uptime_reports_id_seq RESTART WITH 1") +} + +func loadTestData(t *testing.T) { + + for _, account := range testData.Accounts { + err := testDB.gormDB.Create(&account).Error + require.NoError(t, err) + } + + for _, farm := range testData.Farms { + err := testDB.gormDB.Create(&farm).Error + require.NoError(t, err) + } + + for _, node := range testData.Nodes { + err := testDB.gormDB.Create(&node).Error + require.NoError(t, err) + } + + for _, report := range testData.UptimeReports { + err := testDB.gormDB.Create(&report).Error + require.NoError(t, err) + } + + for _, version := range testData.ZosVersions { + err := testDB.gormDB.Create(&version).Error + require.NoError(t, err) + } +} + +func createTestFixtures() *TestFixtures { + now := time.Now() + thirtyMinsAgo := now.Add(-30 * time.Minute) + oneHourAgo := now.Add(-1 * time.Hour) + + return &TestFixtures{ + Accounts: []Account{ + { + PublicKey: "test_public_key_1", + Relays: pq.StringArray{"relay1.example.com", "relay2.example.com"}, + RMBEncKey: "test_rmb_key_1", + CreatedAt: now, + UpdatedAt: now, + }, + { + PublicKey: "test_public_key_2", + Relays: pq.StringArray{"relay3.example.com"}, + RMBEncKey: "test_rmb_key_2", + CreatedAt: now, + UpdatedAt: now, + }, + { + PublicKey: "test_public_key_3", + Relays: pq.StringArray{}, + RMBEncKey: "test_rmb_key_3", + CreatedAt: now, + UpdatedAt: now, + }, + }, + Farms: []Farm{ + { + FarmName: "TestFarm1", + TwinID: 1, + StellarAddress: "G" + strings.Repeat("A", 55), + Dedicated: false, + CreatedAt: now, + UpdatedAt: now, + }, + { + FarmName: "TestFarm2", + TwinID: 2, + StellarAddress: "G" + strings.Repeat("B", 55), + Dedicated: true, + CreatedAt: now, + UpdatedAt: now, + }, + }, + Nodes: []Node{ + { + FarmID: 1, + TwinID: 1, + Location: Location{ + Country: "US", + City: "New York", + Longitude: "-74.0060", + Latitude: "40.7128", + }, + Resources: Resources{ + HRU: 1000, + SRU: 500, + CRU: 8, + MRU: 16, + }, + Interfaces: []Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:55", + IPs: []string{"192.168.1.100", "2001:db8::1"}, + }, + }, + SecureBoot: true, + Virtualized: false, + SerialNumber: "SN001", + LastSeen: thirtyMinsAgo, + CreatedAt: now, + UpdatedAt: now, + Approved: true, + }, + { + FarmID: 1, + TwinID: 2, + Location: Location{ + Country: "DE", + City: "Berlin", + Longitude: "13.4050", + Latitude: "52.5200", + }, + Resources: Resources{ + HRU: 2000, + SRU: 1000, + CRU: 16, + MRU: 32, + }, + Interfaces: []Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:66", + IPs: []string{"192.168.1.101"}, + }, + }, + SecureBoot: false, + Virtualized: true, + SerialNumber: "SN002", + LastSeen: oneHourAgo, + CreatedAt: now, + UpdatedAt: now, + Approved: false, + }, + { + FarmID: 2, + TwinID: 3, + Location: Location{ + Country: "JP", + City: "Tokyo", + Longitude: "139.6917", + Latitude: "35.6895", + }, + Resources: Resources{ + HRU: 3000, + SRU: 1500, + CRU: 32, + MRU: 64, + }, + Interfaces: []Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:77", + IPs: []string{"192.168.1.102", "192.168.1.103"}, + }, + { + Name: "eth1", + Mac: "00:11:22:33:44:88", + IPs: []string{"10.0.0.1"}, + }, + }, + SecureBoot: true, + Virtualized: false, + SerialNumber: "SN003", + LastSeen: now, + CreatedAt: now, + UpdatedAt: now, + Approved: true, + }, + }, + UptimeReports: []UptimeReport{ + { + NodeID: 1, + Duration: 24 * time.Hour, + Timestamp: thirtyMinsAgo, + WasRestart: false, + CreatedAt: thirtyMinsAgo, + }, + { + NodeID: 1, + Duration: 48 * time.Hour, + Timestamp: oneHourAgo, + WasRestart: true, + CreatedAt: oneHourAgo, + }, + { + NodeID: 3, + Duration: 12 * time.Hour, + Timestamp: now, + WasRestart: false, + CreatedAt: now, + }, + }, + ZosVersions: []ZosVersion{ + { + Key: ZOS4VersionKey, + Version: "4.0.0", + }, + { + Key: "zos_3", + Version: "3.14.1", + }, + }, + } +} diff --git a/node-registrar/pkg/db/farms_test.go b/node-registrar/pkg/db/farms_test.go new file mode 100644 index 0000000..2063cb6 --- /dev/null +++ b/node-registrar/pkg/db/farms_test.go @@ -0,0 +1,194 @@ +package db + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCreateFarm(t *testing.T) { + BeforeEach(t) + + t.Run("Create Farm", func(t *testing.T) { + farm := Farm{ + FarmName: "NewTestFarm", + TwinID: 1, + StellarAddress: "G" + strings.Repeat("C", 55), + Dedicated: true, + } + + farmID, err := testDB.CreateFarm(farm) + require.NoError(t, err) + require.NotZero(t, farmID) + + createdFarm, err := testDB.GetFarm(farmID) + require.NoError(t, err) + assert.Equal(t, farm.FarmName, createdFarm.FarmName) + assert.Equal(t, farm.TwinID, createdFarm.TwinID) + assert.Equal(t, farm.StellarAddress, createdFarm.StellarAddress) + assert.Equal(t, farm.Dedicated, createdFarm.Dedicated) + assert.NotZero(t, createdFarm.CreatedAt) + assert.NotZero(t, createdFarm.UpdatedAt) + }) + + t.Run("Create Farm with Duplicate Name", func(t *testing.T) { + farm := Farm{ + FarmName: "TestFarm1", + TwinID: 2, + StellarAddress: "G" + strings.Repeat("D", 55), + Dedicated: false, + } + + farmID, err := testDB.CreateFarm(farm) + require.Error(t, err) + assert.Equal(t, ErrRecordAlreadyExists, err) + assert.Zero(t, farmID) + }) + + t.Run("Create Farm with Invalid Twin ID", func(t *testing.T) { + farm := Farm{ + FarmName: "InvalidTwinIDFarm", + TwinID: 999, + StellarAddress: "G" + strings.Repeat("E", 55), + Dedicated: false, + } + + farmID, err := testDB.CreateFarm(farm) + require.Error(t, err) + assert.Zero(t, farmID) + }) + + t.Run("Create Farm with Missing Required Fields", func(t *testing.T) { + farm := Farm{ + FarmName: "MissingFieldsFarm", + Dedicated: false, + } + + farmID, err := testDB.CreateFarm(farm) + require.Error(t, err) + assert.Zero(t, farmID) + }) +} + +func TestGetFarm(t *testing.T) { + BeforeEach(t) + + t.Run("Get Existing Farm", func(t *testing.T) { + farm, err := testDB.GetFarm(1) + require.NoError(t, err) + assert.Equal(t, uint64(1), farm.FarmID) + assert.Equal(t, "TestFarm1", farm.FarmName) + assert.Equal(t, uint64(1), farm.TwinID) + assert.Equal(t, "G"+strings.Repeat("A", 55), farm.StellarAddress) + assert.False(t, farm.Dedicated) + + farmVerify, err := testDB.GetFarm(1) + require.NoError(t, err) + assert.Equal(t, farm.FarmName, farmVerify.FarmName) + assert.Equal(t, farm.TwinID, farmVerify.TwinID) + }) + + t.Run("Get Non-existent Farm", func(t *testing.T) { + farm, err := testDB.GetFarm(999) + assert.Error(t, err) + assert.Equal(t, ErrRecordNotFound, err) + assert.Equal(t, Farm{}, farm) + }) +} + +func TestListFarms(t *testing.T) { + BeforeEach(t) + + t.Run("List All Farms", func(t *testing.T) { + farms, err := testDB.ListFarms(FarmFilter{}, DefaultLimit()) + require.NoError(t, err) + assert.Len(t, farms, 2) + }) + + t.Run("List Farms with Farm Name Filter", func(t *testing.T) { + farmName := "TestFarm1" + filter := FarmFilter{FarmName: &farmName} + farms, err := testDB.ListFarms(filter, DefaultLimit()) + require.NoError(t, err) + assert.Len(t, farms, 1) + assert.Equal(t, farmName, farms[0].FarmName) + }) + + t.Run("List Farms with Farm ID Filter", func(t *testing.T) { + farmID := uint64(2) + filter := FarmFilter{FarmID: &farmID} + farms, err := testDB.ListFarms(filter, DefaultLimit()) + require.NoError(t, err) + assert.Len(t, farms, 1) + assert.Equal(t, farmID, farms[0].FarmID) + assert.Equal(t, "TestFarm2", farms[0].FarmName) + }) + + t.Run("List Farms with Twin ID Filter", func(t *testing.T) { + twinID := uint64(1) + filter := FarmFilter{TwinID: &twinID} + farms, err := testDB.ListFarms(filter, DefaultLimit()) + require.NoError(t, err) + assert.Len(t, farms, 1) + assert.Equal(t, twinID, farms[0].TwinID) + }) +} + +func TestUpdateFarm(t *testing.T) { + BeforeEach(t) + + t.Run("Update Both Name and Stellar Address", func(t *testing.T) { + originalFarm, err := testDB.GetFarm(1) + require.NoError(t, err) + + newName := "UpdatedTestFarm1" + newStellarAddr := "G" + strings.Repeat("U", 55) + err = testDB.UpdateFarm(1, newName, newStellarAddr) + require.NoError(t, err) + + farm, err := testDB.GetFarm(1) + require.NoError(t, err) + assert.Equal(t, newName, farm.FarmName) + assert.Equal(t, newStellarAddr, farm.StellarAddress) + assert.Equal(t, originalFarm.TwinID, farm.TwinID) + assert.Equal(t, originalFarm.Dedicated, farm.Dedicated) + }) + + t.Run("Update Non-existent Farm", func(t *testing.T) { + err := testDB.UpdateFarm(999, "NonExistentFarm", "G"+strings.Repeat("X", 55)) + require.Error(t, err) + assert.Equal(t, ErrRecordNotFound, err) + }) + + t.Run("Update with Empty Fields", func(t *testing.T) { + originalFarm, err := testDB.GetFarm(1) + require.NoError(t, err) + + err = testDB.UpdateFarm(originalFarm.FarmID, "", "") + require.NoError(t, err) + + updatedFarm, err := testDB.GetFarm(originalFarm.FarmID) + require.NoError(t, err) + assert.Equal(t, originalFarm.FarmName, updatedFarm.FarmName) + assert.Equal(t, originalFarm.StellarAddress, updatedFarm.StellarAddress) + assert.Equal(t, originalFarm.TwinID, updatedFarm.TwinID) + assert.Equal(t, originalFarm.Dedicated, updatedFarm.Dedicated) + }) + + t.Run("Update Farm Name to Duplicate Should Fail", func(t *testing.T) { + farm1, err := testDB.GetFarm(1) + require.NoError(t, err) + farm2, err := testDB.GetFarm(2) + require.NoError(t, err) + + err = testDB.UpdateFarm(farm1.FarmID, farm2.FarmName, "") + require.Error(t, err) + + unchangedFarm, err := testDB.GetFarm(farm1.FarmID) + require.NoError(t, err) + assert.Equal(t, farm1.FarmName, unchangedFarm.FarmName) + assert.NotEqual(t, farm2.FarmName, unchangedFarm.FarmName) + }) +} diff --git a/node-registrar/pkg/db/migrate_test.go b/node-registrar/pkg/db/migrate_test.go new file mode 100644 index 0000000..249e544 --- /dev/null +++ b/node-registrar/pkg/db/migrate_test.go @@ -0,0 +1,96 @@ +package db + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMigrateNodeLastSeen(t *testing.T) { + BeforeEach(t) + + t.Run("Migrate Nodes with Null LastSeen", func(t *testing.T) { + testDB.gormDB.Model(&Node{}).Where("node_id IN (?)", []uint64{1, 2}).Update("last_seen", nil) + + err := testDB.MigrateNodeLastSeen() + require.NoError(t, err) + + var updatedNullCount int64 + testDB.gormDB.Model(&Node{}).Where("last_seen IS NULL").Count(&updatedNullCount) + assert.Equal(t, int64(1), updatedNullCount) + + // have uptime report should be updated + node1, err := testDB.GetNode(1) + require.NoError(t, err) + assert.False(t, node1.LastSeen.IsZero()) + + // no uptime report should not be updated + node2, err := testDB.GetNode(2) + require.NoError(t, err) + assert.True(t, node2.LastSeen.IsZero()) + }) + + t.Run("Migrate Nodes with Zero Time LastSeen", func(t *testing.T) { + zeroTime := time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC) + testDB.gormDB.Model(&Node{}).Where("node_id = ?", 3).Update("last_seen", zeroTime) + + err := testDB.MigrateNodeLastSeen() + require.NoError(t, err) + + node3, err := testDB.GetNode(3) + require.NoError(t, err) + assert.False(t, node3.LastSeen.Equal(zeroTime)) + assert.False(t, node3.LastSeen.IsZero()) + }) + + t.Run("Skip Nodes with Valid LastSeen", func(t *testing.T) { + originalTime := time.Now().Add(-1 * time.Hour) + testDB.gormDB.Model(&Node{}).Where("node_id = ?", 1).Update("last_seen", originalTime) + + err := testDB.MigrateNodeLastSeen() + require.NoError(t, err) + + node1, err := testDB.GetNode(1) + require.NoError(t, err) + assert.WithinDuration(t, originalTime, node1.LastSeen, time.Second) + }) + + t.Run("Skip Nodes without Uptime Reports", func(t *testing.T) { + newNode := Node{ + FarmID: 1, + TwinID: 4, + Location: Location{ + Country: "IT", + City: "Rome", + Longitude: "12.4964", + Latitude: "41.9028", + }, + Resources: Resources{ + HRU: 1000, + SRU: 500, + CRU: 8, + MRU: 16, + }, + Interfaces: []Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:CC", + IPs: []string{"192.168.1.203"}, + }, + }, + SerialNumber: "SN555", + } + + nodeID, err := testDB.RegisterNode(newNode) + require.NoError(t, err) + + err = testDB.MigrateNodeLastSeen() + require.NoError(t, err) + + migratedNode, err := testDB.GetNode(nodeID) + require.NoError(t, err) + assert.True(t, migratedNode.LastSeen.IsZero()) + }) +} diff --git a/node-registrar/pkg/db/nodes_test.go b/node-registrar/pkg/db/nodes_test.go new file mode 100644 index 0000000..5bce0bc --- /dev/null +++ b/node-registrar/pkg/db/nodes_test.go @@ -0,0 +1,367 @@ +package db + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListNodes(t *testing.T) { + BeforeEach(t) + + t.Run("List All Nodes", func(t *testing.T) { + nodes, err := testDB.ListNodes(NodeFilter{}, DefaultLimit()) + require.NoError(t, err) + assert.Len(t, nodes, 3) + }) + + t.Run("List Nodes with Node ID Filter", func(t *testing.T) { + nodeID := uint64(1) + filter := NodeFilter{NodeID: &nodeID} + nodes, err := testDB.ListNodes(filter, DefaultLimit()) + require.NoError(t, err) + assert.Len(t, nodes, 1) + assert.Equal(t, nodeID, nodes[0].NodeID) + assert.Equal(t, "SN001", nodes[0].SerialNumber) + }) + + t.Run("List Nodes with Non-matching Filter", func(t *testing.T) { + nodeID := uint64(999) + filter := NodeFilter{NodeID: &nodeID} + nodes, err := testDB.ListNodes(filter, DefaultLimit()) + require.NoError(t, err) + assert.Len(t, nodes, 0) + }) +} + +func TestGetNode(t *testing.T) { + BeforeEach(t) + + t.Run("Get Existing Node", func(t *testing.T) { + originalNode := testData.Nodes[0] + node, err := testDB.GetNode(1) + require.NoError(t, err) + assert.Equal(t, uint64(1), node.NodeID) + assert.Equal(t, originalNode.FarmID, node.FarmID) + assert.Equal(t, originalNode.TwinID, node.TwinID) + assert.Equal(t, originalNode.SerialNumber, node.SerialNumber) + assert.Equal(t, originalNode.Location.Country, node.Location.Country) + assert.Equal(t, originalNode.Resources.CRU, node.Resources.CRU) + assert.Len(t, node.Interfaces, 1) + assert.Equal(t, originalNode.Interfaces[0].Name, node.Interfaces[0].Name) + + var nodeGorm Node + err = testDB.gormDB.Where("node_id = ?", 1).First(&nodeGorm).Error + require.NoError(t, err) + assert.Equal(t, node.NodeID, nodeGorm.NodeID) + }) + + t.Run("Get Non-existent Node", func(t *testing.T) { + _, err := testDB.GetNode(999) + require.Error(t, err) + assert.Equal(t, ErrRecordNotFound, err) + }) +} + +func TestRegisterNode(t *testing.T) { + BeforeEach(t) + + t.Run("Register Valid Node", func(t *testing.T) { + newNode := Node{ + FarmID: 1, + TwinID: 4, + Location: Location{ + Country: "CA", + City: "Toronto", + Longitude: "-79.3470", + Latitude: "43.6532", + }, + Resources: Resources{ + HRU: 4000, + SRU: 2000, + CRU: 64, + MRU: 128, + }, + Interfaces: []Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:99", + IPs: []string{"192.168.1.200"}, + }, + }, + SecureBoot: true, + Virtualized: false, + SerialNumber: "SN999", + Approved: true, + } + + nodeID, err := testDB.RegisterNode(newNode) + require.NoError(t, err) + assert.Greater(t, nodeID, uint64(0)) + + node, err := testDB.GetNode(nodeID) + require.NoError(t, err) + assert.Equal(t, newNode.FarmID, node.FarmID) + assert.Equal(t, newNode.TwinID, node.TwinID) + assert.Equal(t, newNode.SerialNumber, node.SerialNumber) + assert.Equal(t, newNode.Location.Country, node.Location.Country) + }) + + t.Run("Register Node with Duplicate TwinID", func(t *testing.T) { + newNode := Node{ + FarmID: 1, + TwinID: 1, + Location: Location{ + Country: "FR", + City: "Paris", + Longitude: "2.3522", + Latitude: "48.8566", + }, + Resources: Resources{ + HRU: 1000, + SRU: 500, + CRU: 8, + MRU: 16, + }, + Interfaces: []Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:AA", + IPs: []string{"192.168.1.201"}, + }, + }, + SerialNumber: "SN888", + } + + _, err := testDB.RegisterNode(newNode) + require.Error(t, err) + }) + + t.Run("Register Node with Empty Interfaces", func(t *testing.T) { + newNode := Node{ + FarmID: 1, + TwinID: 4, + Location: Location{ + Country: "UK", + City: "London", + Longitude: "-0.1276", + Latitude: "51.5074", + }, + Resources: Resources{ + HRU: 1000, + SRU: 500, + CRU: 8, + MRU: 16, + }, + Interfaces: []Interface{}, + SerialNumber: "SN777", + } + + _, err := testDB.RegisterNode(newNode) + require.Error(t, err) + assert.Contains(t, err.Error(), "interfaces must not be empty") + }) + + t.Run("Register Node with Invalid FarmID", func(t *testing.T) { + newNode := Node{ + FarmID: 999, + TwinID: 4, + Location: Location{ + Country: "ES", + City: "Madrid", + Longitude: "-3.7038", + Latitude: "40.4168", + }, + Resources: Resources{ + HRU: 1000, + SRU: 500, + CRU: 8, + MRU: 16, + }, + Interfaces: []Interface{ + { + Name: "eth0", + Mac: "00:11:22:33:44:BB", + IPs: []string{"192.168.1.202"}, + }, + }, + SerialNumber: "SN666", + } + + _, err := testDB.RegisterNode(newNode) + require.Error(t, err) + }) +} + +func TestUpdateNode(t *testing.T) { + BeforeEach(t) + + t.Run("Update Node Successfully", func(t *testing.T) { + originalNode, err := testDB.GetNode(1) + require.NoError(t, err) + + updateNode := Node{ + Location: Location{ + Country: "AU", + City: "Sydney", + Longitude: "151.2093", + Latitude: "-33.8688", + }, + Resources: Resources{ + HRU: 5000, + SRU: 2500, + CRU: 128, + MRU: 256, + }, + SecureBoot: false, + Virtualized: true, + SerialNumber: "SN_UPDATED", + Approved: false, + } + + err = testDB.UpdateNode(1, updateNode) + require.NoError(t, err) + + updatedNode, err := testDB.GetNode(1) + require.NoError(t, err) + assert.Equal(t, updateNode.Location.Country, updatedNode.Location.Country) + assert.Equal(t, updateNode.Resources.CRU, updatedNode.Resources.CRU) + assert.Equal(t, updateNode.SerialNumber, updatedNode.SerialNumber) + assert.Equal(t, originalNode.FarmID, updatedNode.FarmID) + assert.Equal(t, originalNode.TwinID, updatedNode.TwinID) + + var nodeGorm Node + err = testDB.gormDB.Where("node_id = ?", 1).First(&nodeGorm).Error + require.NoError(t, err) + assert.Equal(t, updateNode.SerialNumber, nodeGorm.SerialNumber) + }) + + t.Run("Update Non-existent Node", func(t *testing.T) { + updateNode := Node{ + SerialNumber: "NON_EXISTENT", + } + + err := testDB.UpdateNode(999, updateNode) + require.Error(t, err) + assert.Equal(t, ErrRecordNotFound, err) + }) +} + +func TestGetUptimeReports(t *testing.T) { + BeforeEach(t) + + t.Run("Get Uptime Reports for Node", func(t *testing.T) { + start := time.Now().Add(-2 * time.Hour) + end := time.Now() + + reports, err := testDB.GetUptimeReports(1, start, end) + require.NoError(t, err) + assert.Len(t, reports, 2) + + for _, report := range reports { + assert.Equal(t, uint64(1), report.NodeID) + assert.True(t, report.Timestamp.After(start) || report.Timestamp.Equal(start)) + assert.True(t, report.Timestamp.Before(end) || report.Timestamp.Equal(end)) + } + }) + + t.Run("Get Uptime Reports for Node with No Reports", func(t *testing.T) { + start := time.Now().Add(-2 * time.Hour) + end := time.Now() + + reports, err := testDB.GetUptimeReports(2, start, end) + require.NoError(t, err) + assert.Len(t, reports, 0) + }) +} + +func TestCreateUptimeReport(t *testing.T) { + BeforeEach(t) + + t.Run("Create Uptime Report and Updated LastSeen", func(t *testing.T) { + originalNode, err := testDB.GetNode(1) + require.NoError(t, err) + + newTimestamp := time.Now() + report := &UptimeReport{ + NodeID: 1, + Duration: 72 * time.Hour, + Timestamp: newTimestamp, + WasRestart: false, + } + + err = testDB.CreateUptimeReport(report) + require.NoError(t, err) + + reports, err := testDB.GetUptimeReports(1, newTimestamp.Add(-time.Minute), newTimestamp.Add(time.Minute)) + require.NoError(t, err) + require.NotEmpty(t, reports) + assert.Equal(t, report.Duration, reports[len(reports)-1].Duration) + + updatedNode, err := testDB.GetNode(1) + require.NoError(t, err) + assert.True(t, updatedNode.LastSeen.After(originalNode.LastSeen)) + assert.WithinDuration(t, newTimestamp, updatedNode.LastSeen, time.Second) + + }) + + t.Run("Create Uptime Report for Non-existent Node", func(t *testing.T) { + report := &UptimeReport{ + NodeID: 999, + Duration: 24 * time.Hour, + Timestamp: time.Now(), + WasRestart: false, + } + + err := testDB.CreateUptimeReport(report) + require.Error(t, err) + }) +} + +func TestSetZOSVersion(t *testing.T) { + BeforeEach(t) + + t.Run("Set New ZOS Version", func(t *testing.T) { + err := testDB.SetZOSVersion("4.1.0") + require.NoError(t, err) + + version, err := testDB.GetZOSVersion() + require.NoError(t, err) + assert.Equal(t, "4.1.0", version) + + var versionGorm ZosVersion + err = testDB.gormDB.Where("key = ?", ZOS4VersionKey).First(&versionGorm).Error + require.NoError(t, err) + assert.Equal(t, "4.1.0", versionGorm.Version) + }) + + t.Run("Update Existing ZOS Version", func(t *testing.T) { + err := testDB.SetZOSVersion("4.2.0") + require.NoError(t, err) + + version, err := testDB.GetZOSVersion() + require.NoError(t, err) + assert.Equal(t, "4.2.0", version) + }) + + t.Run("Set Same Version Again", func(t *testing.T) { + currentVersion, err := testDB.GetZOSVersion() + require.NoError(t, err) + + err = testDB.SetZOSVersion(currentVersion) + require.Error(t, err) + assert.Contains(t, err.Error(), "version already set") + }) +} + +func TestGetZOSVersion(t *testing.T) { + BeforeEach(t) + + t.Run("Get Existing ZOS Version", func(t *testing.T) { + version, err := testDB.GetZOSVersion() + require.NoError(t, err) + assert.Equal(t, "4.0.0", version) + }) +}