commit d9bb3fc26ddd7fb8213c1decfea5c36e45cbd5b7
parent 2a651d2c6909d8e3a5a735159459f5b54720eb92
Author: Vetle Haflan <vetle@haflan.dev>
Date: Sat, 11 Apr 2020 02:18:53 +0200
Add events.go, tests++
Diffstat:
5 files changed, 220 insertions(+), 11 deletions(-)
diff --git a/database/database.go b/database/database.go
@@ -3,6 +3,8 @@ package database
import (
"errors"
"fmt"
+ "log"
+ "strconv"
"time"
"golang.org/x/crypto/bcrypt"
@@ -125,4 +127,21 @@ func Close() {
// GetDB gets the database structure
func GetDB() *bbolt.DB {
return db
-}-
\ No newline at end of file
+}
+
+// BytesToUint64 converts a byte array to a uint64 number, an operation that is
+// often repeated for IDs. It is assumed that the data will parse successfully
+// (i.e. type checking is performed in an earlier stage).
+// If the parsing fails, the function therefore panics
+func BytesToUint64(byteData []byte) uint64 {
+ uint64Data, err := strconv.ParseUint(string(byteData), 10, 64)
+ if err != nil {
+ log.Panic("couldn't parse byte data to uint64")
+ }
+ return uint64Data
+}
+
+// Uint64ToBytes converts a uint64 formatted number to a byte array
+func Uint64ToBytes(uint64Data uint64) []byte {
+ return []byte(strconv.FormatUint(uint64Data, 10))
+}
diff --git a/events/events.go b/events/events.go
@@ -0,0 +1,150 @@
+package events
+
+import (
+ "errors"
+ "log"
+ "sermoni/database"
+ "sermoni/services"
+ "strconv"
+
+ "go.etcd.io/bbolt"
+)
+
+var (
+ keyEventID = []byte("id")
+ keyEventService = []byte("service")
+ keyEventTimestamp = []byte("timestamp")
+ keyEventStatus = []byte("status")
+ keyEventTitle = []byte("title")
+ keyEventDetails = []byte("details")
+)
+
+// Event contains data sent to sermoni from a service
+type Event struct {
+ ID uint64 `json:"id"`
+ Service uint64 `json:"service"` // ID of a Service (to be mapped to service name client-side)
+ Timestamp uint64 `json:"timestamp"`
+ Status string `json:"status"`
+ Title string `json:"title"`
+ Details string `json:"details"`
+}
+
+// GetAll returns all events in the database
+func GetAll() (events []*Event) {
+ db := database.GetDB()
+ db.View(func(tx *bbolt.Tx) error {
+ b := tx.Bucket(database.BucketKeyEvents)
+ if b == nil {
+ log.Panic("The events bucket does not exist")
+ }
+ // ForEach doesn't return buckets (nil instead), so only the key is useful
+ return b.ForEach(func(id, _ []byte) error {
+ eb := b.Bucket(id)
+ event := &Event{}
+ if err := event.fromBucket(eb); err != nil {
+ return err
+ }
+ events = append(events, event)
+ return nil
+ })
+ })
+ return
+}
+
+// Delete a service event
+func Delete() {
+
+}
+
+// Add a new service event if the token matches any services in the database
+// The event.Service will be set to the service ID automatically, given a valid token
+func Add(serviceToken string, event *Event) error {
+ db := database.GetDB()
+ service := services.GetByToken(serviceToken)
+ if service == nil {
+ return errors.New("no service found for the given token")
+ }
+ return db.Update(func(tx *bbolt.Tx) error {
+ b := tx.Bucket(database.BucketKeyEvents)
+ if b == nil {
+ log.Panic("The events bucket does not exist")
+ }
+
+ // Create a new event ID
+ idInt, err := b.NextSequence()
+ if err != nil {
+ return err
+ }
+ id := []byte(strconv.FormatUint(idInt, 10))
+ event.ID = idInt
+ event.Service = service.ID
+
+ // Create the event bucket and fill it with data from event
+ eb, err := b.CreateBucket(id)
+ if err != nil {
+ return err
+ }
+ return event.toBucket(eb)
+ })
+}
+
+// Writes the event data to the given bucket
+func (event *Event) toBucket(eb *bbolt.Bucket) error {
+ var err error
+ id := database.Uint64ToBytes(event.ID)
+ service := database.Uint64ToBytes(event.Service)
+ if err = eb.Put(keyEventID, id); err != nil {
+ return err
+ }
+ if eb.Put(keyEventService, service); err != nil {
+ return err
+ }
+ if eb.Put(keyEventStatus, []byte(event.Status)); err != nil {
+ return err
+ }
+ if eb.Put(keyEventTitle, []byte(event.Title)); err != nil {
+ return err
+ }
+ if eb.Put(keyEventDetails, []byte(event.Details)); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Reads data from the given bucket into the fields of event
+// Returns error if any of the fields cannot be found
+func (event *Event) fromBucket(eb *bbolt.Bucket) error {
+ var id, service, timestamp []byte
+ var status, title, details []byte
+ err := errors.New("missing field from database")
+
+ // Get data from database
+ if id := eb.Get(keyEventID); id == nil {
+ return err
+ }
+ if service = eb.Get(keyEventService); service == nil {
+ return err
+ }
+ if timestamp = eb.Get(keyEventTimestamp); timestamp == nil {
+ return err
+ }
+ if status = eb.Get(keyEventStatus); status == nil {
+ return err
+ }
+ if title = eb.Get(keyEventTitle); title == nil {
+ return err
+ }
+ if details = eb.Get(keyEventDetails); details == nil {
+ return err
+ }
+
+ // Format data and set fields of event
+ event.ID = database.BytesToUint64(id)
+ event.Service = database.BytesToUint64(service)
+ event.Timestamp = database.BytesToUint64(timestamp)
+ event.Status = string(status)
+ event.Title = string(title)
+ event.Details = string(details)
+
+ return nil
+}
diff --git a/events/events_test.go b/events/events_test.go
@@ -0,0 +1,38 @@
+package events
+
+import (
+ "os"
+ "sermoni/database"
+ "sermoni/services"
+ "testing"
+)
+
+const serviceToken = "test-service"
+
+func TestAddEvent(t *testing.T) {
+ Add(serviceToken, &Event{
+ Timestamp: 1586558825515,
+ Title: "Backup completed successfully",
+ })
+}
+
+func TestMain(m *testing.M) {
+ // (Re)create the test database
+ testDB := "test.db"
+ os.Remove(testDB)
+ var err error
+ if err = database.Init(testDB); err != nil {
+ print("Couldn't initialize test database")
+ os.Exit(1)
+ }
+ err = services.Add(serviceToken, &services.Service{
+ Name: "test @ dev-laptop",
+ Description: "Service used for testing only",
+ })
+ if err != nil {
+ print("Couldn't add test service")
+ os.Exit(1)
+ }
+ defer database.Close()
+ os.Exit(m.Run())
+}
diff --git a/services/services.go b/services/services.go
@@ -24,13 +24,12 @@ type Service struct {
ExpectationPeriod uint64 `json:"period"` // set if the service is expected to report periodically, format is UnixTime (milli?)
}
-
// GetByToken returns the service structure associated with the token string, if there
// are any matching entries in service-tokens bucket. Returns nil if there are no matches
func GetByToken(token string) *Service {
id := getIDFromToken(token)
if id == nil {
- log.Printf("No service found for the token '%v'\n", token)
+ log.Printf("no service found for the token '%v'\n", token)
return nil
}
return get(id)
@@ -43,9 +42,8 @@ func GetByID(id uint64) *Service {
}
// GetAll returns all services in the database (TODO)
-func GetAll() []*Service {
+func GetAll() (services []*Service) {
db := database.GetDB()
- var services []*Service
db.View(func(tx *bbolt.Tx) error {
b := tx.Bucket(database.BucketKeyServices)
stb := tx.Bucket(database.BucketKeyServiceTokens)
@@ -66,7 +64,7 @@ func GetAll() []*Service {
return nil
})
})
- return services
+ return
}
// Delete deletes the given service if it exists
@@ -86,7 +84,7 @@ func Delete(intID uint64) error {
if b.Bucket(serviceID) == nil {
return errors.New("no service for the given id")
}
- if err := b.DeleteBucket(serviceID); err != nil {
+ if err := b.DeleteBucket(serviceID); err != nil {
return err
}
@@ -150,16 +148,15 @@ func Add(token string, service *Service) error {
})
}
-
-
//
// Package-local helpers
//
// fromBucket populates the service struct with data from the given service bucket
+// TODO: Consider failing on missing fields and generally choosing an approach more similar to Event.fromBucket
func (service *Service) fromBucket(id []byte, sb *bbolt.Bucket) {
// Ignoring this error, because it shouldn't be possible
- idInt, _ := strconv.ParseUint(string(id), 10, 64)
+ idInt := database.BytesToUint64(id)
service.ID = idInt
if name := sb.Get(keyServiceName); name != nil {
service.Name = string(name)
diff --git a/services/services_test.go b/services/services_test.go
@@ -99,6 +99,9 @@ func TestGetByID(t *testing.T) {
if !service.equals(testService) {
t.Fatal("stored service doesn't match original")
}
+ if service = GetByID(23423); service != nil {
+ t.Fatal("returned service for invalid id")
+ }
}
func TestGetByToken(t *testing.T) {
@@ -107,6 +110,9 @@ func TestGetByToken(t *testing.T) {
if !service.equals(testService) {
t.Fatal("stored service doesn't match original")
}
+ if service = GetByToken("not-a-token"); service != nil {
+ t.Fatal("returned service for invalid token")
+ }
}
func TestGetAll(t *testing.T) {