mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-06 20:03:05 +08:00
516 lines
15 KiB
Go
516 lines
15 KiB
Go
package jvm
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"GoNavi-Wails/internal/connection"
|
|
)
|
|
|
|
type fakeGuardProvider struct {
|
|
before ValueSnapshot
|
|
beforeErr error
|
|
preview ChangePreview
|
|
previewErr error
|
|
apply ApplyResult
|
|
applyErr error
|
|
}
|
|
|
|
func (f fakeGuardProvider) Mode() string { return ModeJMX }
|
|
func (f fakeGuardProvider) TestConnection(context.Context, connection.ConnectionConfig) error {
|
|
return nil
|
|
}
|
|
func (f fakeGuardProvider) ProbeCapabilities(context.Context, connection.ConnectionConfig) ([]Capability, error) {
|
|
return nil, nil
|
|
}
|
|
func (f fakeGuardProvider) ListResources(context.Context, connection.ConnectionConfig, string) ([]ResourceSummary, error) {
|
|
return nil, nil
|
|
}
|
|
func (f fakeGuardProvider) GetValue(context.Context, connection.ConnectionConfig, string) (ValueSnapshot, error) {
|
|
return f.before, f.beforeErr
|
|
}
|
|
func (f fakeGuardProvider) PreviewChange(context.Context, connection.ConnectionConfig, ChangeRequest) (ChangePreview, error) {
|
|
return f.preview, f.previewErr
|
|
}
|
|
func (f fakeGuardProvider) ApplyChange(context.Context, connection.ConnectionConfig, ChangeRequest) (ApplyResult, error) {
|
|
return f.apply, f.applyErr
|
|
}
|
|
|
|
func TestPreviewChangeBlocksReadOnlyConnection(t *testing.T) {
|
|
readOnly := true
|
|
|
|
preview, err := BuildChangePreview(context.Background(), fakeGuardProvider{}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-readonly",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/cache/orders",
|
|
Action: "put",
|
|
Reason: "fix cache",
|
|
Payload: map[string]any{
|
|
"status": "ready",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error: %v", err)
|
|
}
|
|
if preview.Allowed {
|
|
t.Fatalf("expected preview to be blocked, got %#v", preview)
|
|
}
|
|
if preview.BlockingReason == "" || !strings.Contains(preview.BlockingReason, "只读") {
|
|
t.Fatalf("expected readonly blocking reason, got %#v", preview)
|
|
}
|
|
if strings.TrimSpace(preview.ConfirmationToken) != "" {
|
|
t.Fatalf("expected blocked preview to not include confirmation token, got %#v", preview)
|
|
}
|
|
if preview.Before.ResourceID != "/cache/orders" {
|
|
t.Fatalf("expected before snapshot resource id to be preserved, got %#v", preview.Before)
|
|
}
|
|
if preview.After.ResourceID != "/cache/orders" {
|
|
t.Fatalf("expected after snapshot resource id to be preserved, got %#v", preview.After)
|
|
}
|
|
}
|
|
|
|
func TestPreviewChangeRejectsMissingReason(t *testing.T) {
|
|
readOnly := false
|
|
|
|
_, err := BuildChangePreview(context.Background(), fakeGuardProvider{}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-writable",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/cache/orders",
|
|
Action: "put",
|
|
Reason: " ",
|
|
Payload: map[string]any{
|
|
"status": "ready",
|
|
},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "reason is required") {
|
|
t.Fatalf("expected missing reason to be rejected, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPreviewChangeReturnsProviderPreviewErrorWhenWriteAllowed(t *testing.T) {
|
|
readOnly := false
|
|
|
|
_, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
previewErr: errors.New("preview not implemented"),
|
|
}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-writable",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/cache/orders",
|
|
Action: "put",
|
|
Reason: "fix cache",
|
|
Payload: map[string]any{
|
|
"status": "ready",
|
|
},
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "preview not implemented") {
|
|
t.Fatalf("expected provider preview error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPreviewChangeMarksProdWritesAsConfirmationRequired(t *testing.T) {
|
|
readOnly := false
|
|
|
|
preview, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
preview: ChangePreview{
|
|
Allowed: true,
|
|
Summary: "provider preview",
|
|
RiskLevel: "low",
|
|
},
|
|
}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-prod",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
Environment: EnvPROD,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/cache/orders",
|
|
Action: "put",
|
|
Reason: "fix cache",
|
|
Payload: map[string]any{
|
|
"status": "ready",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error: %v", err)
|
|
}
|
|
if !preview.RequiresConfirmation {
|
|
t.Fatalf("expected prod preview to require confirmation, got %#v", preview)
|
|
}
|
|
if preview.RiskLevel != "low" {
|
|
t.Fatalf("expected provider risk level to be preserved, got %#v", preview)
|
|
}
|
|
}
|
|
|
|
func TestPreviewChangeMarksHighRiskWritesAsConfirmationRequired(t *testing.T) {
|
|
readOnly := false
|
|
|
|
preview, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
preview: ChangePreview{
|
|
Allowed: true,
|
|
Summary: "provider high risk preview",
|
|
RiskLevel: "high",
|
|
},
|
|
}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-writable",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/mbean/java.lang:type=Memory/operation/gc",
|
|
Action: "invoke",
|
|
Reason: "manual maintenance",
|
|
Payload: map[string]any{
|
|
"args": []any{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error: %v", err)
|
|
}
|
|
if !preview.RequiresConfirmation {
|
|
t.Fatalf("expected high risk preview to require confirmation, got %#v", preview)
|
|
}
|
|
if strings.TrimSpace(preview.ConfirmationToken) == "" {
|
|
t.Fatalf("expected high risk preview to include confirmation token, got %#v", preview)
|
|
}
|
|
}
|
|
|
|
func TestPreviewChangeMergesProviderSensitiveFlag(t *testing.T) {
|
|
readOnly := false
|
|
|
|
preview, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
before: ValueSnapshot{
|
|
ResourceID: "/cache/orders/password",
|
|
Kind: "attribute",
|
|
Format: "string",
|
|
Value: "old-secret",
|
|
},
|
|
preview: ChangePreview{
|
|
Allowed: true,
|
|
Summary: "provider preview",
|
|
RiskLevel: "high",
|
|
Before: ValueSnapshot{
|
|
Value: "old-secret",
|
|
Sensitive: true,
|
|
},
|
|
After: ValueSnapshot{
|
|
Value: "new-secret",
|
|
Sensitive: true,
|
|
},
|
|
},
|
|
}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-writable",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/cache/orders/password",
|
|
Action: "set",
|
|
Reason: "rotate secret",
|
|
Payload: map[string]any{
|
|
"value": "new-secret",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error: %v", err)
|
|
}
|
|
if !preview.Before.Sensitive || !preview.After.Sensitive {
|
|
t.Fatalf("expected merged preview snapshots to preserve sensitive flag, got %#v", preview)
|
|
}
|
|
}
|
|
|
|
func TestPreviewChangeMergesProviderSnapshotsWithoutDroppingDefaults(t *testing.T) {
|
|
readOnly := false
|
|
|
|
preview, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
before: ValueSnapshot{
|
|
ResourceID: "/cache/orders",
|
|
Kind: "entry",
|
|
Format: "json",
|
|
Value: map[string]any{
|
|
"status": "stale",
|
|
},
|
|
},
|
|
preview: ChangePreview{
|
|
Allowed: true,
|
|
Summary: "provider preview",
|
|
RiskLevel: "medium",
|
|
Before: ValueSnapshot{
|
|
Value: map[string]any{
|
|
"status": "provider-before",
|
|
},
|
|
},
|
|
After: ValueSnapshot{
|
|
Value: map[string]any{
|
|
"status": "provider-after",
|
|
},
|
|
},
|
|
},
|
|
}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-writable",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/cache/orders",
|
|
Action: "put",
|
|
Reason: "fix cache",
|
|
Payload: map[string]any{
|
|
"status": "ready",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error: %v", err)
|
|
}
|
|
if preview.Before.ResourceID != "/cache/orders" || preview.Before.Format != "json" {
|
|
t.Fatalf("expected before snapshot defaults to be preserved, got %#v", preview.Before)
|
|
}
|
|
if preview.After.ResourceID != "/cache/orders" || preview.After.Format != "json" {
|
|
t.Fatalf("expected after snapshot defaults to be preserved, got %#v", preview.After)
|
|
}
|
|
}
|
|
|
|
func TestBuildChangePreviewAddsConfirmationTokenWhenRequired(t *testing.T) {
|
|
readOnly := false
|
|
|
|
preview, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
preview: ChangePreview{
|
|
Allowed: true,
|
|
Summary: "invoke resize",
|
|
RiskLevel: "high",
|
|
RequiresConfirmation: true,
|
|
},
|
|
}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-prod",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
Environment: EnvPROD,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/mbean/java.lang:type=Memory/operation/gc",
|
|
Action: "invoke",
|
|
Reason: "manual maintenance",
|
|
Payload: map[string]any{
|
|
"args": []any{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error: %v", err)
|
|
}
|
|
if !preview.RequiresConfirmation {
|
|
t.Fatalf("expected confirmation requirement, got %#v", preview)
|
|
}
|
|
if strings.TrimSpace(preview.ConfirmationToken) == "" {
|
|
t.Fatalf("expected confirmation token, got %#v", preview)
|
|
}
|
|
}
|
|
|
|
func TestBuildChangePreviewUsesNormalizedProviderModeForConfirmationToken(t *testing.T) {
|
|
readOnly := false
|
|
cfg := connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-prod",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
Environment: EnvPROD,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}
|
|
|
|
previewWithoutRequestedMode, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
preview: ChangePreview{
|
|
Allowed: true,
|
|
Summary: "invoke resize",
|
|
RiskLevel: "high",
|
|
RequiresConfirmation: true,
|
|
},
|
|
}, cfg, ChangeRequest{
|
|
ProviderMode: "",
|
|
ResourceID: "/mbean/java.lang:type=Memory/operation/gc",
|
|
Action: "invoke",
|
|
Reason: "manual maintenance",
|
|
Payload: map[string]any{
|
|
"args": []any{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error for empty provider mode: %v", err)
|
|
}
|
|
if strings.TrimSpace(previewWithoutRequestedMode.ConfirmationToken) == "" {
|
|
t.Fatalf("expected confirmation token for empty requested provider mode, got %#v", previewWithoutRequestedMode)
|
|
}
|
|
|
|
previewWithRequestedMode, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
preview: ChangePreview{
|
|
Allowed: true,
|
|
Summary: "invoke resize",
|
|
RiskLevel: "high",
|
|
RequiresConfirmation: true,
|
|
},
|
|
}, cfg, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/mbean/java.lang:type=Memory/operation/gc",
|
|
Action: "invoke",
|
|
Reason: "manual maintenance",
|
|
Payload: map[string]any{
|
|
"args": []any{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error for explicit provider mode: %v", err)
|
|
}
|
|
if strings.TrimSpace(previewWithRequestedMode.ConfirmationToken) == "" {
|
|
t.Fatalf("expected confirmation token for explicit requested provider mode, got %#v", previewWithRequestedMode)
|
|
}
|
|
|
|
if previewWithoutRequestedMode.ConfirmationToken != previewWithRequestedMode.ConfirmationToken {
|
|
t.Fatalf("expected tokens to match when normalized mode is the same, got %q vs %q", previewWithoutRequestedMode.ConfirmationToken, previewWithRequestedMode.ConfirmationToken)
|
|
}
|
|
}
|
|
|
|
func TestBuildChangePreviewBlockedByProviderDoesNotGenerateConfirmationToken(t *testing.T) {
|
|
readOnly := false
|
|
|
|
preview, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
preview: ChangePreview{
|
|
Allowed: false,
|
|
RequiresConfirmation: true,
|
|
BlockingReason: "provider denied write",
|
|
Summary: "blocked by provider",
|
|
RiskLevel: "high",
|
|
},
|
|
}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-prod",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
Environment: EnvPROD,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/mbean/java.lang:type=Memory/operation/gc",
|
|
Action: "invoke",
|
|
Reason: "manual maintenance",
|
|
Payload: map[string]any{
|
|
"args": []any{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("BuildChangePreview returned error: %v", err)
|
|
}
|
|
if preview.Allowed {
|
|
t.Fatalf("expected provider-blocked preview, got %#v", preview)
|
|
}
|
|
if strings.TrimSpace(preview.ConfirmationToken) != "" {
|
|
t.Fatalf("expected blocked preview to not include confirmation token, got %#v", preview)
|
|
}
|
|
}
|
|
|
|
func TestBuildChangePreviewFailsClosedWhenTokenMarshalFails(t *testing.T) {
|
|
readOnly := false
|
|
_, err := BuildChangePreview(context.Background(), fakeGuardProvider{
|
|
preview: ChangePreview{
|
|
Allowed: true,
|
|
Summary: "invoke resize",
|
|
RiskLevel: "high",
|
|
RequiresConfirmation: true,
|
|
},
|
|
}, connection.ConnectionConfig{
|
|
Type: "jvm",
|
|
ID: "conn-prod",
|
|
Host: "orders.internal",
|
|
JVM: connection.JVMConfig{
|
|
ReadOnly: &readOnly,
|
|
Environment: EnvPROD,
|
|
PreferredMode: ModeJMX,
|
|
AllowedModes: []string{ModeJMX},
|
|
},
|
|
}, ChangeRequest{
|
|
ProviderMode: ModeJMX,
|
|
ResourceID: "/mbean/java.lang:type=Memory/operation/gc",
|
|
Action: "invoke",
|
|
Reason: "manual maintenance",
|
|
Payload: map[string]any{
|
|
"invalid": func() {},
|
|
},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected BuildChangePreview to fail when confirmation token marshal fails")
|
|
}
|
|
if !strings.Contains(err.Error(), "确认令牌") {
|
|
t.Fatalf("expected error to mention confirmation token, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateChangeConfirmationRejectsMissingOrMismatchedToken(t *testing.T) {
|
|
preview := ChangePreview{
|
|
Allowed: true,
|
|
RequiresConfirmation: true,
|
|
ConfirmationToken: "token-a",
|
|
}
|
|
if err := ValidateChangeConfirmation(preview, ChangeRequest{}); err == nil {
|
|
t.Fatal("expected missing confirmation token to be rejected")
|
|
}
|
|
if err := ValidateChangeConfirmation(preview, ChangeRequest{ConfirmationToken: "token-b"}); err == nil {
|
|
t.Fatal("expected mismatched confirmation token to be rejected")
|
|
}
|
|
if err := ValidateChangeConfirmation(preview, ChangeRequest{ConfirmationToken: "token-a"}); err != nil {
|
|
t.Fatalf("expected matching confirmation token to pass, got %v", err)
|
|
}
|
|
}
|