Consolidate store tests into interface package + Fix issues
This commit is contained in:
		| @ -169,8 +169,8 @@ func (s *Store) GetAllServiceStatuses(params *paging.ServiceStatusParams) map[st | |||||||
| } | } | ||||||
|  |  | ||||||
| // GetServiceStatus returns the service status for a given service name in the given group | // GetServiceStatus returns the service status for a given service name in the given group | ||||||
| func (s *Store) GetServiceStatus(groupName, serviceName string, parameters *paging.ServiceStatusParams) *core.ServiceStatus { | func (s *Store) GetServiceStatus(groupName, serviceName string, params *paging.ServiceStatusParams) *core.ServiceStatus { | ||||||
| 	return s.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(groupName, serviceName), parameters) | 	return s.GetServiceStatusByKey(util.ConvertGroupAndServiceToKey(groupName, serviceName), params) | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetServiceStatusByKey returns the service status for a given key | // GetServiceStatusByKey returns the service status for a given key | ||||||
| @ -468,9 +468,9 @@ func (s *Store) getServiceStatusByKey(tx *sql.Tx, key string, parameters *paging | |||||||
| 	} | 	} | ||||||
| 	if parameters.IncludeUptime { | 	if parameters.IncludeUptime { | ||||||
| 		now := time.Now() | 		now := time.Now() | ||||||
| 		serviceStatus.Uptime.LastHour, _, _ = s.getServiceUptime(tx, serviceID, now.Add(-time.Hour), now) | 		serviceStatus.Uptime.LastHour, _, err = s.getServiceUptime(tx, serviceID, now.Add(-time.Hour), now) | ||||||
| 		serviceStatus.Uptime.LastTwentyFourHours, _, _ = s.getServiceUptime(tx, serviceID, now.Add(-24*time.Hour), now) | 		serviceStatus.Uptime.LastTwentyFourHours, _, err = s.getServiceUptime(tx, serviceID, now.Add(-24*time.Hour), now) | ||||||
| 		serviceStatus.Uptime.LastSevenDays, _, _ = s.getServiceUptime(tx, serviceID, now.Add(-7*24*time.Hour), now) | 		serviceStatus.Uptime.LastSevenDays, _, err = s.getServiceUptime(tx, serviceID, now.Add(-7*24*time.Hour), now) | ||||||
| 	} | 	} | ||||||
| 	return serviceStatus, nil | 	return serviceStatus, nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| package database | package database | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" |  | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @ -14,7 +13,7 @@ var ( | |||||||
| 	secondCondition = core.Condition("[RESPONSE_TIME] < 500") | 	secondCondition = core.Condition("[RESPONSE_TIME] < 500") | ||||||
| 	thirdCondition  = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") | 	thirdCondition  = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") | ||||||
|  |  | ||||||
| 	timestamp = time.Now() | 	now = time.Now() | ||||||
|  |  | ||||||
| 	testService = core.Service{ | 	testService = core.Service{ | ||||||
| 		Name:                    "name", | 		Name:                    "name", | ||||||
| @ -36,7 +35,7 @@ var ( | |||||||
| 		Errors:                nil, | 		Errors:                nil, | ||||||
| 		Connected:             true, | 		Connected:             true, | ||||||
| 		Success:               true, | 		Success:               true, | ||||||
| 		Timestamp:             timestamp, | 		Timestamp:             now, | ||||||
| 		Duration:              150 * time.Millisecond, | 		Duration:              150 * time.Millisecond, | ||||||
| 		CertificateExpiration: 10 * time.Hour, | 		CertificateExpiration: 10 * time.Hour, | ||||||
| 		ConditionResults: []*core.ConditionResult{ | 		ConditionResults: []*core.ConditionResult{ | ||||||
| @ -61,7 +60,7 @@ var ( | |||||||
| 		Errors:                []string{"error-1", "error-2"}, | 		Errors:                []string{"error-1", "error-2"}, | ||||||
| 		Connected:             true, | 		Connected:             true, | ||||||
| 		Success:               false, | 		Success:               false, | ||||||
| 		Timestamp:             timestamp, | 		Timestamp:             now, | ||||||
| 		Duration:              750 * time.Millisecond, | 		Duration:              750 * time.Millisecond, | ||||||
| 		CertificateExpiration: 10 * time.Hour, | 		CertificateExpiration: 10 * time.Hour, | ||||||
| 		ConditionResults: []*core.ConditionResult{ | 		ConditionResults: []*core.ConditionResult{ | ||||||
| @ -95,58 +94,6 @@ func TestNewStore(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestStore_Insert(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_Insert.db") |  | ||||||
| 	defer store.Close() |  | ||||||
| 	store.Insert(&testService, &testSuccessfulResult) |  | ||||||
| 	store.Insert(&testService, &testUnsuccessfulResult) |  | ||||||
|  |  | ||||||
| 	key := fmt.Sprintf("%s_%s", testService.Group, testService.Name) |  | ||||||
| 	serviceStatus := store.GetServiceStatusByKey(key, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) |  | ||||||
| 	if serviceStatus == nil { |  | ||||||
| 		t.Fatalf("Store should've had key '%s', but didn't", key) |  | ||||||
| 	} |  | ||||||
| 	if len(serviceStatus.Results) != 2 { |  | ||||||
| 		t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceStatus.Name, len(serviceStatus.Results)) |  | ||||||
| 	} |  | ||||||
| 	for i, r := range serviceStatus.Results { |  | ||||||
| 		expectedResult := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()).Results[i] |  | ||||||
| 		if r.HTTPStatus != expectedResult.HTTPStatus { |  | ||||||
| 			t.Errorf("Result at index %d should've had a HTTPStatus of %d, but was actually %d", i, expectedResult.HTTPStatus, r.HTTPStatus) |  | ||||||
| 		} |  | ||||||
| 		if r.DNSRCode != expectedResult.DNSRCode { |  | ||||||
| 			t.Errorf("Result at index %d should've had a DNSRCode of %s, but was actually %s", i, expectedResult.DNSRCode, r.DNSRCode) |  | ||||||
| 		} |  | ||||||
| 		if r.Hostname != expectedResult.Hostname { |  | ||||||
| 			t.Errorf("Result at index %d should've had a Hostname of %s, but was actually %s", i, expectedResult.Hostname, r.Hostname) |  | ||||||
| 		} |  | ||||||
| 		if r.IP != expectedResult.IP { |  | ||||||
| 			t.Errorf("Result at index %d should've had a IP of %s, but was actually %s", i, expectedResult.IP, r.IP) |  | ||||||
| 		} |  | ||||||
| 		if r.Connected != expectedResult.Connected { |  | ||||||
| 			t.Errorf("Result at index %d should've had a Connected value of %t, but was actually %t", i, expectedResult.Connected, r.Connected) |  | ||||||
| 		} |  | ||||||
| 		if r.Duration != expectedResult.Duration { |  | ||||||
| 			t.Errorf("Result at index %d should've had a Duration of %s, but was actually %s", i, expectedResult.Duration.String(), r.Duration.String()) |  | ||||||
| 		} |  | ||||||
| 		if len(r.Errors) != len(expectedResult.Errors) { |  | ||||||
| 			t.Errorf("Result at index %d should've had %d errors, but actually had %d errors", i, len(expectedResult.Errors), len(r.Errors)) |  | ||||||
| 		} |  | ||||||
| 		if len(r.ConditionResults) != len(expectedResult.ConditionResults) { |  | ||||||
| 			t.Errorf("Result at index %d should've had %d ConditionResults, but actually had %d ConditionResults", i, len(expectedResult.ConditionResults), len(r.ConditionResults)) |  | ||||||
| 		} |  | ||||||
| 		if r.Success != expectedResult.Success { |  | ||||||
| 			t.Errorf("Result at index %d should've had a Success of %t, but was actually %t", i, expectedResult.Success, r.Success) |  | ||||||
| 		} |  | ||||||
| 		if r.Timestamp != expectedResult.Timestamp { |  | ||||||
| 			t.Errorf("Result at index %d should've had a Timestamp of %s, but was actually %s", i, expectedResult.Timestamp.String(), r.Timestamp.String()) |  | ||||||
| 		} |  | ||||||
| 		if r.CertificateExpiration != expectedResult.CertificateExpiration { |  | ||||||
| 			t.Errorf("Result at index %d should've had a CertificateExpiration of %s, but was actually %s", i, expectedResult.CertificateExpiration.String(), r.CertificateExpiration.String()) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { | func TestStore_InsertCleansUpOldUptimeEntriesProperly(t *testing.T) { | ||||||
| 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpOldUptimeEntriesProperly.db") | 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_InsertCleansUpOldUptimeEntriesProperly.db") | ||||||
| 	defer store.Close() | 	defer store.Close() | ||||||
| @ -220,165 +167,3 @@ func TestStore_InsertCleansUpEventsAndResultsProperly(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| 	store.Clear() | 	store.Clear() | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestStore_GetServiceStatus(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatus.db") |  | ||||||
| 	defer store.Close() |  | ||||||
| 	firstResult := testSuccessfulResult |  | ||||||
| 	firstResult.Timestamp = timestamp.Add(-time.Minute) |  | ||||||
| 	secondResult := testUnsuccessfulResult |  | ||||||
| 	secondResult.Timestamp = timestamp |  | ||||||
| 	store.Insert(&testService, &firstResult) |  | ||||||
| 	store.Insert(&testService, &secondResult) |  | ||||||
|  |  | ||||||
| 	serviceStatus := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) |  | ||||||
| 	if serviceStatus == nil { |  | ||||||
| 		t.Fatalf("serviceStatus shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime == nil { |  | ||||||
| 		t.Fatalf("serviceStatus.Uptime shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if len(serviceStatus.Results) != 2 { |  | ||||||
| 		t.Fatalf("serviceStatus.Results should've had 2 entries") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Results[0].Timestamp.After(serviceStatus.Results[1].Timestamp) { |  | ||||||
| 		t.Error("The result at index 0 should've been older than the result at index 1") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastHour != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastSevenDays != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusForMissingStatusReturnsNil.db") |  | ||||||
| 	defer store.Close() |  | ||||||
| 	store.Insert(&testService, &testSuccessfulResult) |  | ||||||
|  |  | ||||||
| 	serviceStatus := store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) |  | ||||||
| 	if serviceStatus != nil { |  | ||||||
| 		t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, testService.Name) |  | ||||||
| 	} |  | ||||||
| 	serviceStatus = store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) |  | ||||||
| 	if serviceStatus != nil { |  | ||||||
| 		t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname") |  | ||||||
| 	} |  | ||||||
| 	serviceStatus = store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) |  | ||||||
| 	if serviceStatus != nil { |  | ||||||
| 		t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", "nonexistantgroup", testService.Name) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_GetServiceStatusPage1IsHasMoreRecentResultsThanPage2(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusPage1IsHasMoreRecentResultsThanPage2.db") |  | ||||||
| 	defer store.Close() |  | ||||||
| 	firstResult := testSuccessfulResult |  | ||||||
| 	firstResult.Timestamp = timestamp.Add(-time.Minute) |  | ||||||
| 	secondResult := testUnsuccessfulResult |  | ||||||
| 	secondResult.Timestamp = timestamp |  | ||||||
| 	store.Insert(&testService, &firstResult) |  | ||||||
| 	store.Insert(&testService, &secondResult) |  | ||||||
|  |  | ||||||
| 	serviceStatusPage1 := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, 1)) |  | ||||||
| 	if serviceStatusPage1 == nil { |  | ||||||
| 		t.Fatalf("serviceStatusPage1 shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if len(serviceStatusPage1.Results) != 1 { |  | ||||||
| 		t.Fatalf("serviceStatusPage1 should've had 1 result") |  | ||||||
| 	} |  | ||||||
| 	serviceStatusPage2 := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(2, 1)) |  | ||||||
| 	if serviceStatusPage2 == nil { |  | ||||||
| 		t.Fatalf("serviceStatusPage2 shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if len(serviceStatusPage2.Results) != 1 { |  | ||||||
| 		t.Fatalf("serviceStatusPage2 should've had 1 result") |  | ||||||
| 	} |  | ||||||
| 	// Compare the timestamp of both pages |  | ||||||
| 	if !serviceStatusPage1.Results[0].Timestamp.After(serviceStatusPage2.Results[0].Timestamp) { |  | ||||||
| 		t.Errorf("The result from the first page should've been more recent than the results from the second page") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_GetServiceStatusByKey(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusByKey.db") |  | ||||||
| 	defer store.Close() |  | ||||||
| 	store.Insert(&testService, &testSuccessfulResult) |  | ||||||
| 	store.Insert(&testService, &testUnsuccessfulResult) |  | ||||||
|  |  | ||||||
| 	serviceStatus := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) |  | ||||||
| 	if serviceStatus == nil { |  | ||||||
| 		t.Fatalf("serviceStatus shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Name != testService.Name { |  | ||||||
| 		t.Fatalf("serviceStatus.Name should've been %s, got %s", testService.Name, serviceStatus.Name) |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Group != testService.Group { |  | ||||||
| 		t.Fatalf("serviceStatus.Group should've been %s, got %s", testService.Group, serviceStatus.Group) |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime == nil { |  | ||||||
| 		t.Fatalf("serviceStatus.Uptime shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastHour != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastSevenDays != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_GetAllServiceStatuses(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_GetAllServiceStatuses.db") |  | ||||||
| 	defer store.Close() |  | ||||||
| 	firstResult := &testSuccessfulResult |  | ||||||
| 	secondResult := &testUnsuccessfulResult |  | ||||||
| 	store.Insert(&testService, firstResult) |  | ||||||
| 	store.Insert(&testService, secondResult) |  | ||||||
| 	// Can't be bothered dealing with timezone issues on the worker that runs the automated tests |  | ||||||
| 	firstResult.Timestamp = time.Time{} |  | ||||||
| 	secondResult.Timestamp = time.Time{} |  | ||||||
| 	serviceStatuses := store.GetAllServiceStatuses(paging.NewServiceStatusParams().WithResults(1, 20)) |  | ||||||
| 	if len(serviceStatuses) != 1 { |  | ||||||
| 		t.Fatal("expected 1 service status") |  | ||||||
| 	} |  | ||||||
| 	actual, exists := serviceStatuses[testService.Key()] |  | ||||||
| 	if !exists { |  | ||||||
| 		t.Fatal("expected service status to exist") |  | ||||||
| 	} |  | ||||||
| 	if len(actual.Results) != 2 { |  | ||||||
| 		t.Error("expected 2 results, got", len(actual.Results)) |  | ||||||
| 	} |  | ||||||
| 	if len(actual.Events) != 0 { |  | ||||||
| 		t.Error("expected 0 events, got", len(actual.Events)) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_DeleteAllServiceStatusesNotInKeys(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("sqlite", t.TempDir()+"/TestStore_DeleteAllServiceStatusesNotInKeys.db") |  | ||||||
| 	defer store.Close() |  | ||||||
| 	firstService := core.Service{Name: "service-1", Group: "group"} |  | ||||||
| 	secondService := core.Service{Name: "service-2", Group: "group"} |  | ||||||
| 	result := &testSuccessfulResult |  | ||||||
| 	store.Insert(&firstService, result) |  | ||||||
| 	store.Insert(&secondService, result) |  | ||||||
| 	if store.GetServiceStatusByKey(firstService.Key(), paging.NewServiceStatusParams()) == nil { |  | ||||||
| 		t.Fatal("firstService should exist") |  | ||||||
| 	} |  | ||||||
| 	if store.GetServiceStatusByKey(secondService.Key(), paging.NewServiceStatusParams()) == nil { |  | ||||||
| 		t.Fatal("secondService should exist") |  | ||||||
| 	} |  | ||||||
| 	store.DeleteAllServiceStatusesNotInKeys([]string{firstService.Key()}) |  | ||||||
| 	if store.GetServiceStatusByKey(firstService.Key(), paging.NewServiceStatusParams()) == nil { |  | ||||||
| 		t.Error("secondService should've been deleted") |  | ||||||
| 	} |  | ||||||
| 	if store.GetServiceStatusByKey(secondService.Key(), paging.NewServiceStatusParams()) != nil { |  | ||||||
| 		t.Error("firstService should still exist") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -88,10 +88,9 @@ func TestStore_Insert(t *testing.T) { | |||||||
| 	if store.cache.Count() != 1 { | 	if store.cache.Count() != 1 { | ||||||
| 		t.Fatalf("expected 1 ServiceStatus, got %d", store.cache.Count()) | 		t.Fatalf("expected 1 ServiceStatus, got %d", store.cache.Count()) | ||||||
| 	} | 	} | ||||||
| 	key := testService.Key() | 	serviceStatus := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, 20)) | ||||||
| 	serviceStatus := store.GetServiceStatusByKey(key, paging.NewServiceStatusParams()) |  | ||||||
| 	if serviceStatus == nil { | 	if serviceStatus == nil { | ||||||
| 		t.Fatalf("Store should've had key '%s', but didn't", key) | 		t.Fatalf("Store should've had key '%s', but didn't", testService.Key()) | ||||||
| 	} | 	} | ||||||
| 	if len(serviceStatus.Results) != 2 { | 	if len(serviceStatus.Results) != 2 { | ||||||
| 		t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceStatus.Name, len(serviceStatus.Results)) | 		t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceStatus.Name, len(serviceStatus.Results)) | ||||||
| @ -134,148 +133,6 @@ func TestStore_Insert(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestStore_GetServiceStatus(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("") |  | ||||||
| 	store.Insert(&testService, &testSuccessfulResult) |  | ||||||
| 	store.Insert(&testService, &testUnsuccessfulResult) |  | ||||||
|  |  | ||||||
| 	serviceStatus := store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams()) |  | ||||||
| 	if serviceStatus == nil { |  | ||||||
| 		t.Fatalf("serviceStatus shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime == nil { |  | ||||||
| 		t.Fatalf("serviceStatus.Uptime shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastHour != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastSevenDays != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("") |  | ||||||
| 	store.Insert(&testService, &testSuccessfulResult) |  | ||||||
|  |  | ||||||
| 	serviceStatus := store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams()) |  | ||||||
| 	if serviceStatus != nil { |  | ||||||
| 		t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, testService.Name) |  | ||||||
| 	} |  | ||||||
| 	serviceStatus = store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams()) |  | ||||||
| 	if serviceStatus != nil { |  | ||||||
| 		t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname") |  | ||||||
| 	} |  | ||||||
| 	serviceStatus = store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams()) |  | ||||||
| 	if serviceStatus != nil { |  | ||||||
| 		t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", "nonexistantgroup", testService.Name) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_GetServiceStatusByKey(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("") |  | ||||||
| 	store.Insert(&testService, &testSuccessfulResult) |  | ||||||
| 	store.Insert(&testService, &testUnsuccessfulResult) |  | ||||||
|  |  | ||||||
| 	serviceStatus := store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams()) |  | ||||||
| 	if serviceStatus == nil { |  | ||||||
| 		t.Fatalf("serviceStatus shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime == nil { |  | ||||||
| 		t.Fatalf("serviceStatus.Uptime shouldn't have been nil") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastHour != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5") |  | ||||||
| 	} |  | ||||||
| 	if serviceStatus.Uptime.LastSevenDays != 0.5 { |  | ||||||
| 		t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_GetAllServiceStatusesWithResults(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("") |  | ||||||
| 	firstResult := &testSuccessfulResult |  | ||||||
| 	secondResult := &testUnsuccessfulResult |  | ||||||
| 	store.Insert(&testService, firstResult) |  | ||||||
| 	store.Insert(&testService, secondResult) |  | ||||||
| 	// Can't be bothered dealing with timezone issues on the worker that runs the automated tests |  | ||||||
| 	firstResult.Timestamp = time.Time{} |  | ||||||
| 	secondResult.Timestamp = time.Time{} |  | ||||||
| 	serviceStatuses := store.GetAllServiceStatuses(paging.NewServiceStatusParams().WithResults(1, 20)) |  | ||||||
| 	if len(serviceStatuses) != 1 { |  | ||||||
| 		t.Fatal("expected 1 service status") |  | ||||||
| 	} |  | ||||||
| 	actual, exists := serviceStatuses[testService.Key()] |  | ||||||
| 	if !exists { |  | ||||||
| 		t.Fatal("expected service status to exist") |  | ||||||
| 	} |  | ||||||
| 	if len(actual.Results) != 2 { |  | ||||||
| 		t.Error("expected 2 results, got", len(actual.Results)) |  | ||||||
| 	} |  | ||||||
| 	if len(actual.Events) != 0 { |  | ||||||
| 		t.Error("expected 0 events, got", len(actual.Events)) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_GetAllServiceStatusesWithResultsAndEvents(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("") |  | ||||||
| 	firstResult := &testSuccessfulResult |  | ||||||
| 	secondResult := &testUnsuccessfulResult |  | ||||||
| 	store.Insert(&testService, firstResult) |  | ||||||
| 	store.Insert(&testService, secondResult) |  | ||||||
| 	// Can't be bothered dealing with timezone issues on the worker that runs the automated tests |  | ||||||
| 	firstResult.Timestamp = time.Time{} |  | ||||||
| 	secondResult.Timestamp = time.Time{} |  | ||||||
| 	serviceStatuses := store.GetAllServiceStatuses(paging.NewServiceStatusParams().WithResults(1, 20).WithEvents(1, 50)) |  | ||||||
| 	if len(serviceStatuses) != 1 { |  | ||||||
| 		t.Fatal("expected 1 service status") |  | ||||||
| 	} |  | ||||||
| 	actual, exists := serviceStatuses[testService.Key()] |  | ||||||
| 	if !exists { |  | ||||||
| 		t.Fatal("expected service status to exist") |  | ||||||
| 	} |  | ||||||
| 	if len(actual.Results) != 2 { |  | ||||||
| 		t.Error("expected 2 results, got", len(actual.Results)) |  | ||||||
| 	} |  | ||||||
| 	if len(actual.Events) != 2 { |  | ||||||
| 		t.Error("expected 2 events, got", len(actual.Events)) |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_DeleteAllServiceStatusesNotInKeys(t *testing.T) { |  | ||||||
| 	store, _ := NewStore("") |  | ||||||
| 	firstService := core.Service{Name: "service-1", Group: "group"} |  | ||||||
| 	secondService := core.Service{Name: "service-2", Group: "group"} |  | ||||||
| 	result := &testSuccessfulResult |  | ||||||
| 	store.Insert(&firstService, result) |  | ||||||
| 	store.Insert(&secondService, result) |  | ||||||
| 	if store.cache.Count() != 2 { |  | ||||||
| 		t.Errorf("expected cache to have 2 keys, got %d", store.cache.Count()) |  | ||||||
| 	} |  | ||||||
| 	if store.GetServiceStatusByKey(firstService.Key(), paging.NewServiceStatusParams()) == nil { |  | ||||||
| 		t.Fatal("firstService should exist") |  | ||||||
| 	} |  | ||||||
| 	if store.GetServiceStatusByKey(secondService.Key(), paging.NewServiceStatusParams()) == nil { |  | ||||||
| 		t.Fatal("secondService should exist") |  | ||||||
| 	} |  | ||||||
| 	store.DeleteAllServiceStatusesNotInKeys([]string{firstService.Key()}) |  | ||||||
| 	if store.cache.Count() != 1 { |  | ||||||
| 		t.Fatalf("expected cache to have 1 keys, got %d", store.cache.Count()) |  | ||||||
| 	} |  | ||||||
| 	if store.GetServiceStatusByKey(firstService.Key(), paging.NewServiceStatusParams()) == nil { |  | ||||||
| 		t.Error("secondService should've been deleted") |  | ||||||
| 	} |  | ||||||
| 	if store.GetServiceStatusByKey(secondService.Key(), paging.NewServiceStatusParams()) != nil { |  | ||||||
| 		t.Error("firstService should still exist") |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestStore_Save(t *testing.T) { | func TestStore_Save(t *testing.T) { | ||||||
| 	files := []string{ | 	files := []string{ | ||||||
| 		"", | 		"", | ||||||
|  | |||||||
| @ -61,15 +61,8 @@ func AddResult(ss *core.ServiceStatus, result *core.Result) { | |||||||
| 	} | 	} | ||||||
| 	if len(ss.Results) > 0 { | 	if len(ss.Results) > 0 { | ||||||
| 		// Check if there's any change since the last result | 		// Check if there's any change since the last result | ||||||
| 		// OR there's only 1 event, which only happens when there's a start event | 		if ss.Results[len(ss.Results)-1].Success != result.Success { | ||||||
| 		if ss.Results[len(ss.Results)-1].Success != result.Success || len(ss.Events) == 1 { | 			ss.Events = append(ss.Events, generateEventBasedOnResultSuccess(result)) | ||||||
| 			event := &core.Event{Timestamp: result.Timestamp} |  | ||||||
| 			if result.Success { |  | ||||||
| 				event.Type = core.EventHealthy |  | ||||||
| 			} else { |  | ||||||
| 				event.Type = core.EventUnhealthy |  | ||||||
| 			} |  | ||||||
| 			ss.Events = append(ss.Events, event) |  | ||||||
| 			if len(ss.Events) > core.MaximumNumberOfEvents { | 			if len(ss.Events) > core.MaximumNumberOfEvents { | ||||||
| 				// Doing ss.Events[1:] would usually be sufficient, but in the case where for some reason, the slice has | 				// Doing ss.Events[1:] would usually be sufficient, but in the case where for some reason, the slice has | ||||||
| 				// more than one extra element, we can get rid of all of them at once and thus returning the slice to a | 				// more than one extra element, we can get rid of all of them at once and thus returning the slice to a | ||||||
| @ -77,6 +70,9 @@ func AddResult(ss *core.ServiceStatus, result *core.Result) { | |||||||
| 				ss.Events = ss.Events[len(ss.Events)-core.MaximumNumberOfEvents:] | 				ss.Events = ss.Events[len(ss.Events)-core.MaximumNumberOfEvents:] | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | 	} else { | ||||||
|  | 		// This is the first result, so we need to add the first healthy/unhealthy event | ||||||
|  | 		ss.Events = append(ss.Events, generateEventBasedOnResultSuccess(result)) | ||||||
| 	} | 	} | ||||||
| 	ss.Results = append(ss.Results, result) | 	ss.Results = append(ss.Results, result) | ||||||
| 	if len(ss.Results) > core.MaximumNumberOfResults { | 	if len(ss.Results) > core.MaximumNumberOfResults { | ||||||
| @ -87,3 +83,13 @@ func AddResult(ss *core.ServiceStatus, result *core.Result) { | |||||||
| 	} | 	} | ||||||
| 	processUptimeAfterResult(ss.Uptime, result) | 	processUptimeAfterResult(ss.Uptime, result) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func generateEventBasedOnResultSuccess(result *core.Result) *core.Event { | ||||||
|  | 	event := &core.Event{Timestamp: result.Timestamp} | ||||||
|  | 	if result.Success { | ||||||
|  | 		event.Type = core.EventHealthy | ||||||
|  | 	} else { | ||||||
|  | 		event.Type = core.EventUnhealthy | ||||||
|  | 	} | ||||||
|  | 	return event | ||||||
|  | } | ||||||
|  | |||||||
| @ -10,74 +10,6 @@ import ( | |||||||
| 	"github.com/TwinProduction/gatus/storage/store/paging" | 	"github.com/TwinProduction/gatus/storage/store/paging" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var ( |  | ||||||
| 	firstCondition  = core.Condition("[STATUS] == 200") |  | ||||||
| 	secondCondition = core.Condition("[RESPONSE_TIME] < 500") |  | ||||||
| 	thirdCondition  = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") |  | ||||||
|  |  | ||||||
| 	testService = core.Service{ |  | ||||||
| 		Name:                    "name", |  | ||||||
| 		Group:                   "group", |  | ||||||
| 		URL:                     "https://example.org/what/ever", |  | ||||||
| 		Method:                  "GET", |  | ||||||
| 		Body:                    "body", |  | ||||||
| 		Interval:                30 * time.Second, |  | ||||||
| 		Conditions:              []*core.Condition{&firstCondition, &secondCondition, &thirdCondition}, |  | ||||||
| 		Alerts:                  nil, |  | ||||||
| 		Insecure:                false, |  | ||||||
| 		NumberOfFailuresInARow:  0, |  | ||||||
| 		NumberOfSuccessesInARow: 0, |  | ||||||
| 	} |  | ||||||
| 	testSuccessfulResult = core.Result{ |  | ||||||
| 		Hostname:              "example.org", |  | ||||||
| 		IP:                    "127.0.0.1", |  | ||||||
| 		HTTPStatus:            200, |  | ||||||
| 		Errors:                nil, |  | ||||||
| 		Connected:             true, |  | ||||||
| 		Success:               true, |  | ||||||
| 		Duration:              150 * time.Millisecond, |  | ||||||
| 		CertificateExpiration: 10 * time.Hour, |  | ||||||
| 		ConditionResults: []*core.ConditionResult{ |  | ||||||
| 			{ |  | ||||||
| 				Condition: "[STATUS] == 200", |  | ||||||
| 				Success:   true, |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				Condition: "[RESPONSE_TIME] < 500", |  | ||||||
| 				Success:   true, |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				Condition: "[CERTIFICATE_EXPIRATION] < 72h", |  | ||||||
| 				Success:   true, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| 	testUnsuccessfulResult = core.Result{ |  | ||||||
| 		Hostname:              "example.org", |  | ||||||
| 		IP:                    "127.0.0.1", |  | ||||||
| 		HTTPStatus:            200, |  | ||||||
| 		Errors:                []string{"error-1", "error-2"}, |  | ||||||
| 		Connected:             true, |  | ||||||
| 		Success:               false, |  | ||||||
| 		Duration:              750 * time.Millisecond, |  | ||||||
| 		CertificateExpiration: 10 * time.Hour, |  | ||||||
| 		ConditionResults: []*core.ConditionResult{ |  | ||||||
| 			{ |  | ||||||
| 				Condition: "[STATUS] == 200", |  | ||||||
| 				Success:   true, |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				Condition: "[RESPONSE_TIME] < 500", |  | ||||||
| 				Success:   false, |  | ||||||
| 			}, |  | ||||||
| 			{ |  | ||||||
| 				Condition: "[CERTIFICATE_EXPIRATION] < 72h", |  | ||||||
| 				Success:   false, |  | ||||||
| 			}, |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| func BenchmarkStore_GetAllServiceStatuses(b *testing.B) { | func BenchmarkStore_GetAllServiceStatuses(b *testing.B) { | ||||||
| 	memoryStore, err := memory.NewStore("") | 	memoryStore, err := memory.NewStore("") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -207,3 +139,63 @@ func BenchmarkStore_Insert(b *testing.B) { | |||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func BenchmarkStore_GetServiceStatusByKey(b *testing.B) { | ||||||
|  | 	memoryStore, err := memory.NewStore("") | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	databaseStore, err := database.NewStore("sqlite", b.TempDir()+"/BenchmarkStore_GetServiceStatusByKey.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		b.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	defer databaseStore.Close() | ||||||
|  | 	type Scenario struct { | ||||||
|  | 		Name     string | ||||||
|  | 		Store    Store | ||||||
|  | 		Parallel bool | ||||||
|  | 	} | ||||||
|  | 	scenarios := []Scenario{ | ||||||
|  | 		{ | ||||||
|  | 			Name:     "memory", | ||||||
|  | 			Store:    memoryStore, | ||||||
|  | 			Parallel: false, | ||||||
|  | 		}, | ||||||
|  | 		//{ | ||||||
|  | 		//	Name:     "memory-parallel", | ||||||
|  | 		//	Store:    memoryStore, | ||||||
|  | 		//	Parallel: true, | ||||||
|  | 		//}, | ||||||
|  | 		{ | ||||||
|  | 			Name:     "database", | ||||||
|  | 			Store:    databaseStore, | ||||||
|  | 			Parallel: false, | ||||||
|  | 		}, | ||||||
|  | 		//{ | ||||||
|  | 		//	Name:     "database-parallel", | ||||||
|  | 		//	Store:    databaseStore, | ||||||
|  | 		//	Parallel: true, | ||||||
|  | 		//}, | ||||||
|  | 	} | ||||||
|  | 	for _, scenario := range scenarios { | ||||||
|  | 		for i := 0; i < 10; i++ { | ||||||
|  | 			scenario.Store.Insert(&testService, &testSuccessfulResult) | ||||||
|  | 			scenario.Store.Insert(&testService, &testUnsuccessfulResult) | ||||||
|  | 		} | ||||||
|  | 		b.Run(scenario.Name, func(b *testing.B) { | ||||||
|  | 			if scenario.Parallel { | ||||||
|  | 				b.RunParallel(func(pb *testing.PB) { | ||||||
|  | 					for pb.Next() { | ||||||
|  | 						scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, 20)) | ||||||
|  | 					} | ||||||
|  | 				}) | ||||||
|  | 			} else { | ||||||
|  | 				for n := 0; n < b.N; n++ { | ||||||
|  | 					scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, 20)) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			b.ReportAllocs() | ||||||
|  | 		}) | ||||||
|  | 		scenario.Store.Clear() | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | |||||||
							
								
								
									
										476
									
								
								storage/store/store_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										476
									
								
								storage/store/store_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,476 @@ | |||||||
|  | package store | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/TwinProduction/gatus/core" | ||||||
|  | 	"github.com/TwinProduction/gatus/storage/store/database" | ||||||
|  | 	"github.com/TwinProduction/gatus/storage/store/memory" | ||||||
|  | 	"github.com/TwinProduction/gatus/storage/store/paging" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	firstCondition  = core.Condition("[STATUS] == 200") | ||||||
|  | 	secondCondition = core.Condition("[RESPONSE_TIME] < 500") | ||||||
|  | 	thirdCondition  = core.Condition("[CERTIFICATE_EXPIRATION] < 72h") | ||||||
|  |  | ||||||
|  | 	now = time.Now() | ||||||
|  |  | ||||||
|  | 	testService = core.Service{ | ||||||
|  | 		Name:                    "name", | ||||||
|  | 		Group:                   "group", | ||||||
|  | 		URL:                     "https://example.org/what/ever", | ||||||
|  | 		Method:                  "GET", | ||||||
|  | 		Body:                    "body", | ||||||
|  | 		Interval:                30 * time.Second, | ||||||
|  | 		Conditions:              []*core.Condition{&firstCondition, &secondCondition, &thirdCondition}, | ||||||
|  | 		Alerts:                  nil, | ||||||
|  | 		Insecure:                false, | ||||||
|  | 		NumberOfFailuresInARow:  0, | ||||||
|  | 		NumberOfSuccessesInARow: 0, | ||||||
|  | 	} | ||||||
|  | 	testSuccessfulResult = core.Result{ | ||||||
|  | 		Timestamp:             now, | ||||||
|  | 		Success:               true, | ||||||
|  | 		Hostname:              "example.org", | ||||||
|  | 		IP:                    "127.0.0.1", | ||||||
|  | 		HTTPStatus:            200, | ||||||
|  | 		Errors:                nil, | ||||||
|  | 		Connected:             true, | ||||||
|  | 		Duration:              150 * time.Millisecond, | ||||||
|  | 		CertificateExpiration: 10 * time.Hour, | ||||||
|  | 		ConditionResults: []*core.ConditionResult{ | ||||||
|  | 			{ | ||||||
|  | 				Condition: "[STATUS] == 200", | ||||||
|  | 				Success:   true, | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				Condition: "[RESPONSE_TIME] < 500", | ||||||
|  | 				Success:   true, | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				Condition: "[CERTIFICATE_EXPIRATION] < 72h", | ||||||
|  | 				Success:   true, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	testUnsuccessfulResult = core.Result{ | ||||||
|  | 		Timestamp:             now, | ||||||
|  | 		Success:               false, | ||||||
|  | 		Hostname:              "example.org", | ||||||
|  | 		IP:                    "127.0.0.1", | ||||||
|  | 		HTTPStatus:            200, | ||||||
|  | 		Errors:                []string{"error-1", "error-2"}, | ||||||
|  | 		Connected:             true, | ||||||
|  | 		Duration:              750 * time.Millisecond, | ||||||
|  | 		CertificateExpiration: 10 * time.Hour, | ||||||
|  | 		ConditionResults: []*core.ConditionResult{ | ||||||
|  | 			{ | ||||||
|  | 				Condition: "[STATUS] == 200", | ||||||
|  | 				Success:   true, | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				Condition: "[RESPONSE_TIME] < 500", | ||||||
|  | 				Success:   false, | ||||||
|  | 			}, | ||||||
|  | 			{ | ||||||
|  | 				Condition: "[CERTIFICATE_EXPIRATION] < 72h", | ||||||
|  | 				Success:   false, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestStore_GetServiceStatusByKey(t *testing.T) { | ||||||
|  | 	memoryStore, err := memory.NewStore("") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	databaseStore, err := database.NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusByKey.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	defer databaseStore.Close() | ||||||
|  | 	type Scenario struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Store Store | ||||||
|  | 	} | ||||||
|  | 	scenarios := []Scenario{ | ||||||
|  | 		{ | ||||||
|  | 			Name:  "memory", | ||||||
|  | 			Store: memoryStore, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:  "database", | ||||||
|  | 			Store: databaseStore, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	firstResult := testSuccessfulResult | ||||||
|  | 	firstResult.Timestamp = now.Add(-time.Minute) | ||||||
|  | 	secondResult := testUnsuccessfulResult | ||||||
|  | 	secondResult.Timestamp = now | ||||||
|  | 	for _, scenario := range scenarios { | ||||||
|  | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
|  | 			scenario.Store.Insert(&testService, &firstResult) | ||||||
|  | 			scenario.Store.Insert(&testService, &secondResult) | ||||||
|  |  | ||||||
|  | 			serviceStatus := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) | ||||||
|  | 			if serviceStatus == nil { | ||||||
|  | 				t.Fatalf("serviceStatus shouldn't have been nil") | ||||||
|  | 			} | ||||||
|  | 			if serviceStatus.Name != testService.Name { | ||||||
|  | 				t.Fatalf("serviceStatus.Name should've been %s, got %s", testService.Name, serviceStatus.Name) | ||||||
|  | 			} | ||||||
|  | 			if serviceStatus.Group != testService.Group { | ||||||
|  | 				t.Fatalf("serviceStatus.Group should've been %s, got %s", testService.Group, serviceStatus.Group) | ||||||
|  | 			} | ||||||
|  | 			if len(serviceStatus.Results) != 2 { | ||||||
|  | 				t.Fatalf("serviceStatus.Results should've had 2 entries") | ||||||
|  | 			} | ||||||
|  | 			if serviceStatus.Results[0].Timestamp.After(serviceStatus.Results[1].Timestamp) { | ||||||
|  | 				t.Error("The result at index 0 should've been older than the result at index 1") | ||||||
|  | 			} | ||||||
|  | 			if serviceStatus.Uptime == nil { | ||||||
|  | 				t.Fatalf("serviceStatus.Uptime shouldn't have been nil") | ||||||
|  | 			} | ||||||
|  | 			if serviceStatus.Uptime.LastHour != 0.5 { | ||||||
|  | 				t.Errorf("serviceStatus.Uptime.LastHour should've been 0.5, got %f", serviceStatus.Uptime.LastHour) | ||||||
|  | 			} | ||||||
|  | 			if serviceStatus.Uptime.LastTwentyFourHours != 0.5 { | ||||||
|  | 				t.Errorf("serviceStatus.Uptime.LastTwentyFourHours should've been 0.5, got %f", serviceStatus.Uptime.LastTwentyFourHours) | ||||||
|  | 			} | ||||||
|  | 			if serviceStatus.Uptime.LastSevenDays != 0.5 { | ||||||
|  | 				t.Errorf("serviceStatus.Uptime.LastSevenDays should've been 0.5, got %f", serviceStatus.Uptime.LastSevenDays) | ||||||
|  | 			} | ||||||
|  | 			scenario.Store.Clear() | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestStore_GetServiceStatusForMissingStatusReturnsNil(t *testing.T) { | ||||||
|  | 	memoryStore, err := memory.NewStore("") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	databaseStore, err := database.NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusForMissingStatusReturnsNil.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	defer databaseStore.Close() | ||||||
|  | 	type Scenario struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Store Store | ||||||
|  | 	} | ||||||
|  | 	scenarios := []Scenario{ | ||||||
|  | 		{ | ||||||
|  | 			Name:  "memory", | ||||||
|  | 			Store: memoryStore, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:  "database", | ||||||
|  | 			Store: databaseStore, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, scenario := range scenarios { | ||||||
|  | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
|  | 			scenario.Store.Insert(&testService, &testSuccessfulResult) | ||||||
|  | 			serviceStatus := scenario.Store.GetServiceStatus("nonexistantgroup", "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) | ||||||
|  | 			if serviceStatus != nil { | ||||||
|  | 				t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, testService.Name) | ||||||
|  | 			} | ||||||
|  | 			serviceStatus = scenario.Store.GetServiceStatus(testService.Group, "nonexistantname", paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) | ||||||
|  | 			if serviceStatus != nil { | ||||||
|  | 				t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", testService.Group, "nonexistantname") | ||||||
|  | 			} | ||||||
|  | 			serviceStatus = scenario.Store.GetServiceStatus("nonexistantgroup", testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) | ||||||
|  | 			if serviceStatus != nil { | ||||||
|  | 				t.Errorf("Returned service status for group '%s' and name '%s' not nil after inserting the service into the store", "nonexistantgroup", testService.Name) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestStore_GetAllServiceStatuses(t *testing.T) { | ||||||
|  | 	memoryStore, err := memory.NewStore("") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	databaseStore, err := database.NewStore("sqlite", t.TempDir()+"/TestStore_GetAllServiceStatuses.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	defer databaseStore.Close() | ||||||
|  | 	type Scenario struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Store Store | ||||||
|  | 	} | ||||||
|  | 	scenarios := []Scenario{ | ||||||
|  | 		{ | ||||||
|  | 			Name:  "memory", | ||||||
|  | 			Store: memoryStore, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:  "database", | ||||||
|  | 			Store: databaseStore, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	firstResult := testSuccessfulResult | ||||||
|  | 	secondResult := testUnsuccessfulResult | ||||||
|  | 	for _, scenario := range scenarios { | ||||||
|  | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
|  | 			scenario.Store.Insert(&testService, &firstResult) | ||||||
|  | 			scenario.Store.Insert(&testService, &secondResult) | ||||||
|  | 			// Can't be bothered dealing with timezone issues on the worker that runs the automated tests | ||||||
|  | 			serviceStatuses := scenario.Store.GetAllServiceStatuses(paging.NewServiceStatusParams().WithResults(1, 20)) | ||||||
|  | 			if len(serviceStatuses) != 1 { | ||||||
|  | 				t.Fatal("expected 1 service status") | ||||||
|  | 			} | ||||||
|  | 			actual, exists := serviceStatuses[testService.Key()] | ||||||
|  | 			if !exists { | ||||||
|  | 				t.Fatal("expected service status to exist") | ||||||
|  | 			} | ||||||
|  | 			if len(actual.Results) != 2 { | ||||||
|  | 				t.Error("expected 2 results, got", len(actual.Results)) | ||||||
|  | 			} | ||||||
|  | 			if len(actual.Events) != 0 { | ||||||
|  | 				t.Error("expected 0 events, got", len(actual.Events)) | ||||||
|  | 			} | ||||||
|  | 			scenario.Store.Clear() | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestStore_GetAllServiceStatusesWithResultsAndEvents(t *testing.T) { | ||||||
|  | 	memoryStore, err := memory.NewStore("") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	databaseStore, err := database.NewStore("sqlite", t.TempDir()+"/TestStore_GetAllServiceStatusesWithResultsAndEvents.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	defer databaseStore.Close() | ||||||
|  | 	type Scenario struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Store Store | ||||||
|  | 	} | ||||||
|  | 	scenarios := []Scenario{ | ||||||
|  | 		{ | ||||||
|  | 			Name:  "memory", | ||||||
|  | 			Store: memoryStore, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:  "database", | ||||||
|  | 			Store: databaseStore, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	firstResult := testSuccessfulResult | ||||||
|  | 	secondResult := testUnsuccessfulResult | ||||||
|  | 	for _, scenario := range scenarios { | ||||||
|  | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
|  | 			scenario.Store.Insert(&testService, &firstResult) | ||||||
|  | 			scenario.Store.Insert(&testService, &secondResult) | ||||||
|  | 			// Can't be bothered dealing with timezone issues on the worker that runs the automated tests | ||||||
|  | 			serviceStatuses := scenario.Store.GetAllServiceStatuses(paging.NewServiceStatusParams().WithResults(1, 20).WithEvents(1, 50)) | ||||||
|  | 			if len(serviceStatuses) != 1 { | ||||||
|  | 				t.Fatal("expected 1 service status") | ||||||
|  | 			} | ||||||
|  | 			actual, exists := serviceStatuses[testService.Key()] | ||||||
|  | 			if !exists { | ||||||
|  | 				t.Fatal("expected service status to exist") | ||||||
|  | 			} | ||||||
|  | 			if len(actual.Results) != 2 { | ||||||
|  | 				t.Error("expected 2 results, got", len(actual.Results)) | ||||||
|  | 			} | ||||||
|  | 			if len(actual.Events) != 3 { | ||||||
|  | 				t.Error("expected 3 events, got", len(actual.Events)) | ||||||
|  | 			} | ||||||
|  | 			scenario.Store.Clear() | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestStore_GetServiceStatusPage1IsHasMoreRecentResultsThanPage2(t *testing.T) { | ||||||
|  | 	memoryStore, err := memory.NewStore("") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	databaseStore, err := database.NewStore("sqlite", t.TempDir()+"/TestStore_GetServiceStatusPage1IsHasMoreRecentResultsThanPage2.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	defer databaseStore.Close() | ||||||
|  | 	type Scenario struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Store Store | ||||||
|  | 	} | ||||||
|  | 	scenarios := []Scenario{ | ||||||
|  | 		{ | ||||||
|  | 			Name:  "memory", | ||||||
|  | 			Store: memoryStore, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:  "database", | ||||||
|  | 			Store: databaseStore, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	firstResult := testSuccessfulResult | ||||||
|  | 	firstResult.Timestamp = now.Add(-time.Minute) | ||||||
|  | 	secondResult := testUnsuccessfulResult | ||||||
|  | 	secondResult.Timestamp = now | ||||||
|  | 	for _, scenario := range scenarios { | ||||||
|  | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
|  | 			scenario.Store.Insert(&testService, &firstResult) | ||||||
|  | 			scenario.Store.Insert(&testService, &secondResult) | ||||||
|  | 			serviceStatusPage1 := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(1, 1)) | ||||||
|  | 			if serviceStatusPage1 == nil { | ||||||
|  | 				t.Fatalf("serviceStatusPage1 shouldn't have been nil") | ||||||
|  | 			} | ||||||
|  | 			if len(serviceStatusPage1.Results) != 1 { | ||||||
|  | 				t.Fatalf("serviceStatusPage1 should've had 1 result") | ||||||
|  | 			} | ||||||
|  | 			serviceStatusPage2 := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithResults(2, 1)) | ||||||
|  | 			if serviceStatusPage2 == nil { | ||||||
|  | 				t.Fatalf("serviceStatusPage2 shouldn't have been nil") | ||||||
|  | 			} | ||||||
|  | 			if len(serviceStatusPage2.Results) != 1 { | ||||||
|  | 				t.Fatalf("serviceStatusPage2 should've had 1 result") | ||||||
|  | 			} | ||||||
|  | 			// Compare the timestamp of both pages | ||||||
|  | 			if !serviceStatusPage1.Results[0].Timestamp.After(serviceStatusPage2.Results[0].Timestamp) { | ||||||
|  | 				t.Errorf("The result from the first page should've been more recent than the results from the second page") | ||||||
|  | 			} | ||||||
|  | 			scenario.Store.Clear() | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestStore_Insert(t *testing.T) { | ||||||
|  | 	memoryStore, err := memory.NewStore("") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	databaseStore, err := database.NewStore("sqlite", t.TempDir()+"/TestStore_Insert.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	defer databaseStore.Close() | ||||||
|  | 	type Scenario struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Store Store | ||||||
|  | 	} | ||||||
|  | 	scenarios := []Scenario{ | ||||||
|  | 		{ | ||||||
|  | 			Name:  "memory", | ||||||
|  | 			Store: memoryStore, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:  "database", | ||||||
|  | 			Store: databaseStore, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	firstResult := testSuccessfulResult | ||||||
|  | 	firstResult.Timestamp = now.Add(-time.Minute) | ||||||
|  | 	secondResult := testUnsuccessfulResult | ||||||
|  | 	secondResult.Timestamp = now | ||||||
|  | 	for _, scenario := range scenarios { | ||||||
|  | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
|  | 			scenario.Store.Insert(&testService, &testSuccessfulResult) | ||||||
|  | 			scenario.Store.Insert(&testService, &testUnsuccessfulResult) | ||||||
|  |  | ||||||
|  | 			serviceStatus := scenario.Store.GetServiceStatusByKey(testService.Key(), paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()) | ||||||
|  | 			if serviceStatus == nil { | ||||||
|  | 				t.Fatalf("Store should've had key '%s', but didn't", testService.Key()) | ||||||
|  | 			} | ||||||
|  | 			if len(serviceStatus.Results) != 2 { | ||||||
|  | 				t.Fatalf("Service '%s' should've had 2 results, but actually returned %d", serviceStatus.Name, len(serviceStatus.Results)) | ||||||
|  | 			} | ||||||
|  | 			for i, r := range serviceStatus.Results { | ||||||
|  | 				expectedResult := scenario.Store.GetServiceStatus(testService.Group, testService.Name, paging.NewServiceStatusParams().WithEvents(1, core.MaximumNumberOfEvents).WithResults(1, core.MaximumNumberOfResults).WithUptime()).Results[i] | ||||||
|  | 				if r.HTTPStatus != expectedResult.HTTPStatus { | ||||||
|  | 					t.Errorf("Result at index %d should've had a HTTPStatus of %d, but was actually %d", i, expectedResult.HTTPStatus, r.HTTPStatus) | ||||||
|  | 				} | ||||||
|  | 				if r.DNSRCode != expectedResult.DNSRCode { | ||||||
|  | 					t.Errorf("Result at index %d should've had a DNSRCode of %s, but was actually %s", i, expectedResult.DNSRCode, r.DNSRCode) | ||||||
|  | 				} | ||||||
|  | 				if r.Hostname != expectedResult.Hostname { | ||||||
|  | 					t.Errorf("Result at index %d should've had a Hostname of %s, but was actually %s", i, expectedResult.Hostname, r.Hostname) | ||||||
|  | 				} | ||||||
|  | 				if r.IP != expectedResult.IP { | ||||||
|  | 					t.Errorf("Result at index %d should've had a IP of %s, but was actually %s", i, expectedResult.IP, r.IP) | ||||||
|  | 				} | ||||||
|  | 				if r.Connected != expectedResult.Connected { | ||||||
|  | 					t.Errorf("Result at index %d should've had a Connected value of %t, but was actually %t", i, expectedResult.Connected, r.Connected) | ||||||
|  | 				} | ||||||
|  | 				if r.Duration != expectedResult.Duration { | ||||||
|  | 					t.Errorf("Result at index %d should've had a Duration of %s, but was actually %s", i, expectedResult.Duration.String(), r.Duration.String()) | ||||||
|  | 				} | ||||||
|  | 				if len(r.Errors) != len(expectedResult.Errors) { | ||||||
|  | 					t.Errorf("Result at index %d should've had %d errors, but actually had %d errors", i, len(expectedResult.Errors), len(r.Errors)) | ||||||
|  | 				} | ||||||
|  | 				if len(r.ConditionResults) != len(expectedResult.ConditionResults) { | ||||||
|  | 					t.Errorf("Result at index %d should've had %d ConditionResults, but actually had %d ConditionResults", i, len(expectedResult.ConditionResults), len(r.ConditionResults)) | ||||||
|  | 				} | ||||||
|  | 				if r.Success != expectedResult.Success { | ||||||
|  | 					t.Errorf("Result at index %d should've had a Success of %t, but was actually %t", i, expectedResult.Success, r.Success) | ||||||
|  | 				} | ||||||
|  | 				if r.Timestamp != expectedResult.Timestamp { | ||||||
|  | 					t.Errorf("Result at index %d should've had a Timestamp of %s, but was actually %s", i, expectedResult.Timestamp.String(), r.Timestamp.String()) | ||||||
|  | 				} | ||||||
|  | 				if r.CertificateExpiration != expectedResult.CertificateExpiration { | ||||||
|  | 					t.Errorf("Result at index %d should've had a CertificateExpiration of %s, but was actually %s", i, expectedResult.CertificateExpiration.String(), r.CertificateExpiration.String()) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestStore_DeleteAllServiceStatusesNotInKeys(t *testing.T) { | ||||||
|  | 	memoryStore, err := memory.NewStore("") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	databaseStore, err := database.NewStore("sqlite", t.TempDir()+"/TestStore_DeleteAllServiceStatusesNotInKeys.db") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatal("failed to create store:", err.Error()) | ||||||
|  | 	} | ||||||
|  | 	defer databaseStore.Close() | ||||||
|  | 	type Scenario struct { | ||||||
|  | 		Name  string | ||||||
|  | 		Store Store | ||||||
|  | 	} | ||||||
|  | 	scenarios := []Scenario{ | ||||||
|  | 		{ | ||||||
|  | 			Name:  "memory", | ||||||
|  | 			Store: memoryStore, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:  "database", | ||||||
|  | 			Store: databaseStore, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	firstService := core.Service{Name: "service-1", Group: "group"} | ||||||
|  | 	secondService := core.Service{Name: "service-2", Group: "group"} | ||||||
|  | 	result := &testSuccessfulResult | ||||||
|  | 	for _, scenario := range scenarios { | ||||||
|  | 		t.Run(scenario.Name, func(t *testing.T) { | ||||||
|  | 			scenario.Store.Insert(&firstService, result) | ||||||
|  | 			scenario.Store.Insert(&secondService, result) | ||||||
|  | 			if scenario.Store.GetServiceStatusByKey(firstService.Key(), paging.NewServiceStatusParams()) == nil { | ||||||
|  | 				t.Fatal("firstService should exist") | ||||||
|  | 			} | ||||||
|  | 			if scenario.Store.GetServiceStatusByKey(secondService.Key(), paging.NewServiceStatusParams()) == nil { | ||||||
|  | 				t.Fatal("secondService should exist") | ||||||
|  | 			} | ||||||
|  | 			scenario.Store.DeleteAllServiceStatusesNotInKeys([]string{firstService.Key()}) | ||||||
|  | 			if scenario.Store.GetServiceStatusByKey(firstService.Key(), paging.NewServiceStatusParams()) == nil { | ||||||
|  | 				t.Error("secondService should've been deleted") | ||||||
|  | 			} | ||||||
|  | 			if scenario.Store.GetServiceStatusByKey(secondService.Key(), paging.NewServiceStatusParams()) != nil { | ||||||
|  | 				t.Error("firstService should still exist") | ||||||
|  | 			} | ||||||
|  | 			scenario.Store.Clear() | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user