package daemonserver import ( "context" "errors" "io" "log/slog" "os" "path/filepath" "syscall" "testing" "time" "yms-daemon/internal/backendupdate" "yms-daemon/internal/daemonapi" "yms-daemon/internal/daemonclient" "yms-daemon/internal/transaction" ) func TestServerAcceptsBackendUpdateThroughUnixSocket(t *testing.T) { socketPath := shortSocketPath(t) updater := &fakeUpdater{record: transaction.Transaction{ID: "transaction-01", State: transaction.StateCommitted}} server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatalf("create daemon server: %v", err) } ctx, cancel := context.WithCancel(context.Background()) serveResult := make(chan error, 1) go func() { serveResult <- server.Serve(ctx) }() waitForSocket(t, socketPath, serveResult) packagePath := filepath.Join(t.TempDir(), "package.zip") if err := os.WriteFile(packagePath, []byte("zip"), 0o600); err != nil { t.Fatalf("write update package: %v", err) } var progress []daemonapi.Response response, err := daemonclient.Update(context.Background(), socketPath, "backend", daemonapi.InputTypeRepackZIP, packagePath, func(event daemonapi.Response) { progress = append(progress, event) }) if err != nil { t.Fatalf("submit backend update: %v", err) } if response.TransactionID != updater.record.ID || response.State != string(transaction.StateCommitted) || updater.file != packagePath || updater.inputType != daemonapi.InputTypeRepackZIP { t.Fatalf("unexpected update response or dispatch: response=%+v file=%s", response, updater.file) } if len(progress) != 1 || progress[0].Message != "test update progress" { t.Fatalf("unexpected streamed update progress: %+v", progress) } cancel() select { case err := <-serveResult: if err != nil { t.Fatalf("stop daemon server: %v", err) } case <-time.After(3 * time.Second): t.Fatal("daemon server did not stop") } } func TestServerAcceptsDirectNativeJARThroughUnixSocket(t *testing.T) { socketPath := shortSocketPath(t) updater := &fakeUpdater{record: transaction.Transaction{ID: "transaction-direct-01", State: transaction.StateCommitted}} server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatalf("create daemon server: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() serveResult := make(chan error, 1) go func() { serveResult <- server.Serve(ctx) }() waitForSocket(t, socketPath, serveResult) jarPath := filepath.Join(t.TempDir(), "glory-soft-yms.jar") if err := os.WriteFile(jarPath, []byte("jar"), 0o600); err != nil { t.Fatalf("write direct native backend JAR: %v", err) } response, err := daemonclient.Update(context.Background(), socketPath, "backend", daemonapi.InputTypeNativeJAR, jarPath, nil) if err != nil { t.Fatalf("submit direct native backend update: %v", err) } if response.TransactionID != updater.record.ID || updater.file != jarPath || updater.inputType != daemonapi.InputTypeNativeJAR { t.Fatalf("unexpected direct update response or dispatch: response=%+v file=%s inputType=%s", response, updater.file, updater.inputType) } } func TestServerAcceptsContainerImageThroughUnixSocket(t *testing.T) { socketPath := shortSocketPath(t) updater := &fakeUpdater{record: transaction.Transaction{ID: "transaction-container-01", State: transaction.StateCommitted}} server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatalf("create daemon server: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() serveResult := make(chan error, 1) go func() { serveResult <- server.Serve(ctx) }() waitForSocket(t, socketPath, serveResult) imageReference := "harbor.ymswell.asia/ymswell/glory-ymswell:20260813-184902-a37bf50d-v1.1.8.1" response, err := daemonclient.UpdateContainerImage(context.Background(), socketPath, "backend", imageReference, true, nil) if err != nil { t.Fatalf("submit container backend image: %v", err) } if response.TransactionID != updater.record.ID || updater.file != imageReference || updater.inputType != daemonapi.InputTypeContainerImage { t.Fatalf("unexpected container update dispatch: response=%+v image=%s inputType=%s", response, updater.file, updater.inputType) } } func TestServerReturnsTransactionFailure(t *testing.T) { socketPath := shortSocketPath(t) updater := &fakeUpdater{ record: transaction.Transaction{ID: "transaction-02", State: transaction.StateRolledBack}, err: errors.New("health check failed"), } server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatalf("create daemon server: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() serveResult := make(chan error, 1) go func() { serveResult <- server.Serve(ctx) }() waitForSocket(t, socketPath, serveResult) packagePath := filepath.Join(t.TempDir(), "package.zip") response, err := daemonclient.Update(context.Background(), socketPath, "backend", daemonapi.InputTypeRepackZIP, packagePath, nil) if err == nil || response.TransactionID != updater.record.ID || response.State != string(transaction.StateRolledBack) { t.Fatalf("unexpected failed update response: response=%+v err=%v", response, err) } } func TestServerAcceptsBackendRestartThroughUnixSocket(t *testing.T) { socketPath := shortSocketPath(t) updater := &fakeUpdater{record: transaction.Transaction{ID: "transaction-restart-01", State: transaction.StateCommitted}} server, err := New(socketPath, updater, slog.New(slog.NewTextHandler(io.Discard, nil))) if err != nil { t.Fatalf("create daemon server: %v", err) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() serveResult := make(chan error, 1) go func() { serveResult <- server.Serve(ctx) }() waitForSocket(t, socketPath, serveResult) var progress []daemonapi.Response response, err := daemonclient.Restart(context.Background(), socketPath, "backend", func(event daemonapi.Response) { progress = append(progress, event) }) if err != nil { t.Fatalf("submit backend restart: %v", err) } if response.TransactionID != updater.record.ID || updater.operation != daemonapi.OperationRestart { t.Fatalf("unexpected restart response or dispatch: response=%+v operation=%s", response, updater.operation) } if len(progress) != 1 || progress[0].Message != "test restart progress" { t.Fatalf("unexpected streamed restart progress: %+v", progress) } } func shortSocketPath(t *testing.T) string { t.Helper() directory, err := os.MkdirTemp("", "yd-") if err != nil { t.Fatalf("create short Unix Socket directory: %v", err) } t.Cleanup(func() { _ = os.RemoveAll(directory) }) return filepath.Join(directory, "daemon.sock") } func waitForSocket(t *testing.T, socketPath string, serveResult <-chan error) { t.Helper() deadline := time.Now().Add(3 * time.Second) for time.Now().Before(deadline) { select { case err := <-serveResult: if errors.Is(err, syscall.EPERM) { t.Skip("Unix Socket creation is not permitted by the test sandbox") } t.Fatalf("daemon server stopped before creating Unix Socket: %v", err) default: } info, err := os.Lstat(socketPath) if err == nil && info.Mode()&os.ModeSocket != 0 { return } time.Sleep(5 * time.Millisecond) } t.Fatalf("daemon Unix Socket was not created: %s", socketPath) } type fakeUpdater struct { record transaction.Transaction err error file string inputType string operation string } func (u *fakeUpdater) UpdateRepack(_ context.Context, file string, report backendupdate.ProgressReporter) (transaction.Transaction, error) { u.file = file u.inputType = daemonapi.InputTypeRepackZIP u.operation = daemonapi.OperationUpdate if report != nil { report(backendupdate.Progress{TransactionID: u.record.ID, State: transaction.StateStarting, Message: "test update progress"}) } return u.record, u.err } func (u *fakeUpdater) UpdateNativeJAR(_ context.Context, file string, report backendupdate.ProgressReporter) (transaction.Transaction, error) { u.file = file u.inputType = daemonapi.InputTypeNativeJAR u.operation = daemonapi.OperationUpdate if report != nil { report(backendupdate.Progress{TransactionID: u.record.ID, State: transaction.StateStarting, Message: "test update progress"}) } return u.record, u.err } func (u *fakeUpdater) UpdateContainerImage(_ context.Context, imageReference string, _ bool, report backendupdate.ProgressReporter) (transaction.Transaction, error) { u.file = imageReference u.inputType = daemonapi.InputTypeContainerImage u.operation = daemonapi.OperationUpdate if report != nil { report(backendupdate.Progress{TransactionID: u.record.ID, State: transaction.StateStarting, Message: "test update progress"}) } return u.record, u.err } func (u *fakeUpdater) Restart(_ context.Context, report backendupdate.ProgressReporter) (transaction.Transaction, error) { u.operation = daemonapi.OperationRestart if report != nil { report(backendupdate.Progress{TransactionID: u.record.ID, State: transaction.StateStarting, Message: "test restart progress"}) } return u.record, u.err }