mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
290 lines
7.7 KiB
Go
290 lines
7.7 KiB
Go
package truenas
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
func TestIntegrationFullLifecycle(t *testing.T) {
|
|
server := newMockServer(t, defaultAPIResponses(), nil)
|
|
t.Cleanup(server.Close)
|
|
|
|
client := mustClientForServer(t, server.URL, ClientConfig{APIKey: "api-key"})
|
|
fetcher := &APIFetcher{Client: client}
|
|
provider := NewLiveProvider(fetcher)
|
|
|
|
if err := provider.Refresh(context.Background()); err != nil {
|
|
t.Fatalf("Refresh() error = %v", err)
|
|
}
|
|
|
|
enableTrueNASFeatureFlag(t)
|
|
records := provider.Records()
|
|
if len(records) == 0 {
|
|
t.Fatal("expected non-empty records after successful refresh")
|
|
}
|
|
|
|
registry := unifiedresources.NewRegistry(unifiedresources.NewMemoryStore())
|
|
registry.IngestRecords(unifiedresources.SourceTrueNAS, records)
|
|
|
|
resources := registry.List()
|
|
if len(resources) == 0 {
|
|
t.Fatal("expected resources in registry after ingest")
|
|
}
|
|
|
|
foundHost := false
|
|
foundAppWorkload := false
|
|
foundPoolStorage := false
|
|
foundDatasetStorage := false
|
|
foundPoolHealthTag := false
|
|
|
|
for _, resource := range resources {
|
|
if resource.Type == unifiedresources.ResourceTypeAgent && resource.Name == "truenas-main" {
|
|
foundHost = true
|
|
}
|
|
if resource.Type == unifiedresources.ResourceTypeAppContainer && resource.Name == "Nextcloud" {
|
|
foundAppWorkload = true
|
|
}
|
|
|
|
if resource.Type != unifiedresources.ResourceTypeStorage {
|
|
continue
|
|
}
|
|
|
|
if resource.Storage == nil {
|
|
t.Fatalf("expected StorageMeta for storage resource %q", resource.Name)
|
|
}
|
|
if !resource.Storage.IsZFS {
|
|
t.Fatalf("expected IsZFS=true for storage resource %q", resource.Name)
|
|
}
|
|
|
|
if hasTag(resource.Tags, "pool") {
|
|
foundPoolStorage = true
|
|
if hasTagPrefix(resource.Tags, "health:") {
|
|
foundPoolHealthTag = true
|
|
}
|
|
}
|
|
if hasTag(resource.Tags, "dataset") {
|
|
foundDatasetStorage = true
|
|
}
|
|
}
|
|
|
|
if !foundHost {
|
|
t.Fatal("expected host resource named truenas-main")
|
|
}
|
|
if !foundAppWorkload {
|
|
t.Fatal("expected TrueNAS app-container workload named Nextcloud")
|
|
}
|
|
if !foundPoolStorage {
|
|
t.Fatal("expected at least one storage resource with pool tag")
|
|
}
|
|
if !foundDatasetStorage {
|
|
t.Fatal("expected at least one storage resource with dataset tag")
|
|
}
|
|
if !foundPoolHealthTag {
|
|
t.Fatal("expected at least one pool resource with health tag")
|
|
}
|
|
}
|
|
|
|
func TestIntegrationConnectionRefused(t *testing.T) {
|
|
client, err := NewClient(ClientConfig{
|
|
Host: "127.0.0.1",
|
|
Port: 65534,
|
|
UseHTTPS: false,
|
|
APIKey: "api-key",
|
|
Timeout: 250 * time.Millisecond,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewClient() error = %v", err)
|
|
}
|
|
|
|
provider := NewLiveProvider(&APIFetcher{Client: client})
|
|
if err := provider.Refresh(context.Background()); err == nil {
|
|
t.Fatal("expected Refresh() error for unreachable endpoint")
|
|
}
|
|
|
|
enableTrueNASFeatureFlag(t)
|
|
if records := provider.Records(); records != nil {
|
|
t.Fatalf("expected nil records with no cached snapshot, got %d", len(records))
|
|
}
|
|
}
|
|
|
|
func TestIntegrationAuthFailure(t *testing.T) {
|
|
responses := make(map[string]apiResponse)
|
|
for path := range defaultAPIResponses() {
|
|
responses[path] = apiResponse{
|
|
status: http.StatusUnauthorized,
|
|
body: `{"error":"unauthorized"}`,
|
|
}
|
|
}
|
|
|
|
server := newMockServer(t, responses, nil)
|
|
t.Cleanup(server.Close)
|
|
|
|
client := mustClientForServer(t, server.URL, ClientConfig{APIKey: "bad-key"})
|
|
provider := NewLiveProvider(&APIFetcher{Client: client})
|
|
|
|
err := provider.Refresh(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected Refresh() auth failure error")
|
|
}
|
|
if !strings.Contains(err.Error(), "401") {
|
|
t.Fatalf("expected auth error to contain 401, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationMalformedResponse(t *testing.T) {
|
|
responses := copyAPIResponses(defaultAPIResponses())
|
|
responses["/api/v2.0/system/info"] = apiResponse{
|
|
status: http.StatusOK,
|
|
body: `{not valid json`,
|
|
}
|
|
|
|
server := newMockServer(t, responses, nil)
|
|
t.Cleanup(server.Close)
|
|
|
|
client := mustClientForServer(t, server.URL, ClientConfig{APIKey: "api-key"})
|
|
provider := NewLiveProvider(&APIFetcher{Client: client})
|
|
|
|
if err := provider.Refresh(context.Background()); err == nil {
|
|
t.Fatal("expected Refresh() error for malformed JSON")
|
|
}
|
|
}
|
|
|
|
func TestIntegrationHealthStateTransition(t *testing.T) {
|
|
dynamic := &dynamicResponses{
|
|
responses: copyAPIResponses(defaultAPIResponses()),
|
|
}
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
response, ok := dynamic.get(request.URL.Path)
|
|
if !ok {
|
|
http.NotFound(writer, request)
|
|
return
|
|
}
|
|
|
|
status := response.status
|
|
if status == 0 {
|
|
status = http.StatusOK
|
|
}
|
|
contentType := response.contentType
|
|
if contentType == "" {
|
|
contentType = "application/json"
|
|
}
|
|
|
|
writer.Header().Set("Content-Type", contentType)
|
|
writer.WriteHeader(status)
|
|
_, _ = writer.Write([]byte(response.body))
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
|
|
client := mustClientForServer(t, server.URL, ClientConfig{APIKey: "api-key"})
|
|
provider := NewLiveProvider(&APIFetcher{Client: client})
|
|
enableTrueNASFeatureFlag(t)
|
|
|
|
transitions := []struct {
|
|
poolStatus string
|
|
want unifiedresources.ResourceStatus
|
|
}{
|
|
{poolStatus: "ONLINE", want: unifiedresources.StatusOnline},
|
|
{poolStatus: "DEGRADED", want: unifiedresources.StatusWarning},
|
|
{poolStatus: "FAULTED", want: unifiedresources.StatusOffline},
|
|
{poolStatus: "ONLINE", want: unifiedresources.StatusOnline},
|
|
}
|
|
|
|
for _, transition := range transitions {
|
|
dynamic.set("/api/v2.0/pool", apiResponse{
|
|
status: http.StatusOK,
|
|
body: poolResponseBody(transition.poolStatus),
|
|
})
|
|
|
|
if err := provider.Refresh(context.Background()); err != nil {
|
|
t.Fatalf("Refresh() error for pool status %q: %v", transition.poolStatus, err)
|
|
}
|
|
|
|
records := provider.Records()
|
|
if len(records) == 0 {
|
|
t.Fatalf("expected records for pool status %q", transition.poolStatus)
|
|
}
|
|
|
|
poolRecord := requirePoolRecord(t, records, "tank")
|
|
if poolRecord.Resource.Status != transition.want {
|
|
t.Fatalf("pool status for %q = %q, want %q", transition.poolStatus, poolRecord.Resource.Status, transition.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIntegrationStaleRecovery(t *testing.T) {
|
|
t.Skip("redundant with TestProviderRefreshPreservesLastSnapshotOnError in provider_test.go")
|
|
}
|
|
|
|
type dynamicResponses struct {
|
|
mu sync.Mutex
|
|
responses map[string]apiResponse
|
|
}
|
|
|
|
func (d *dynamicResponses) set(path string, resp apiResponse) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.responses[path] = resp
|
|
}
|
|
|
|
func (d *dynamicResponses) get(path string) (apiResponse, bool) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
resp, ok := d.responses[path]
|
|
return resp, ok
|
|
}
|
|
|
|
func enableTrueNASFeatureFlag(t *testing.T) {
|
|
t.Helper()
|
|
previous := IsFeatureEnabled()
|
|
SetFeatureEnabled(true)
|
|
t.Cleanup(func() {
|
|
SetFeatureEnabled(previous)
|
|
})
|
|
}
|
|
|
|
func requirePoolRecord(t *testing.T, records []unifiedresources.IngestRecord, poolName string) unifiedresources.IngestRecord {
|
|
t.Helper()
|
|
for _, record := range records {
|
|
if record.Resource.Type != unifiedresources.ResourceTypeStorage {
|
|
continue
|
|
}
|
|
if record.Resource.Name != poolName {
|
|
continue
|
|
}
|
|
if !hasTag(record.Resource.Tags, "pool") {
|
|
continue
|
|
}
|
|
return record
|
|
}
|
|
t.Fatalf("missing pool record for %q", poolName)
|
|
return unifiedresources.IngestRecord{}
|
|
}
|
|
|
|
func poolResponseBody(status string) string {
|
|
return `[{"id":1,"name":"tank","status":"` + status + `","size":1000,"allocated":400,"free":600}]`
|
|
}
|
|
|
|
func copyAPIResponses(in map[string]apiResponse) map[string]apiResponse {
|
|
out := make(map[string]apiResponse, len(in))
|
|
for path, response := range in {
|
|
out[path] = response
|
|
}
|
|
return out
|
|
}
|
|
|
|
func hasTagPrefix(tags []string, prefix string) bool {
|
|
for _, tag := range tags {
|
|
if strings.HasPrefix(tag, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|