201 lines
6.7 KiB
Go
201 lines
6.7 KiB
Go
//go:build linux
|
|
|
|
package integration_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
|
|
"yms-daemon/internal/filestore"
|
|
"yms-daemon/internal/nativebackendexecutor"
|
|
"yms-daemon/internal/systemd"
|
|
"yms-daemon/internal/transaction"
|
|
)
|
|
|
|
func TestNativeBackendExecutorIntegration(t *testing.T) {
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
databasePath := filepath.Join(root, "transactions.db")
|
|
store, err := transaction.OpenStore(ctx, databasePath)
|
|
if err != nil {
|
|
t.Fatalf("open transaction store: %v", err)
|
|
}
|
|
coordinator, err := transaction.NewCoordinator(store, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
|
if err != nil {
|
|
t.Fatalf("create transaction coordinator: %v", err)
|
|
}
|
|
releaseStore, err := filestore.New(filepath.Join(root, "releases"))
|
|
if err != nil {
|
|
t.Fatalf("create native backend release store: %v", err)
|
|
}
|
|
|
|
artifactContent := []byte("integrated native backend jar")
|
|
artifactPath := filepath.Join(root, "incoming", "glory-soft-yms.jar")
|
|
if err := os.MkdirAll(filepath.Dir(artifactPath), 0o750); err != nil {
|
|
t.Fatalf("create incoming directory: %v", err)
|
|
}
|
|
if err := os.WriteFile(artifactPath, artifactContent, 0o640); err != nil {
|
|
t.Fatalf("write incoming backend JAR: %v", err)
|
|
}
|
|
previousTarget := filepath.Join(root, "previous", "glory-soft-yms.jar")
|
|
if err := os.MkdirAll(filepath.Dir(previousTarget), 0o750); err != nil {
|
|
t.Fatalf("create previous release directory: %v", err)
|
|
}
|
|
if err := os.WriteFile(previousTarget, []byte("previous native backend jar"), 0o640); err != nil {
|
|
t.Fatalf("write previous backend JAR: %v", err)
|
|
}
|
|
slotJarPath := filepath.Join(root, "slots", "backend-green.jar")
|
|
if err := os.MkdirAll(filepath.Dir(slotJarPath), 0o750); err != nil {
|
|
t.Fatalf("create native backend slot directory: %v", err)
|
|
}
|
|
if err := os.Symlink(previousTarget, slotJarPath); err != nil {
|
|
t.Fatalf("create native backend slot link: %v", err)
|
|
}
|
|
|
|
request := nativebackendexecutor.Request{
|
|
ArtifactPath: artifactPath,
|
|
ArtifactIdentity: integrationIdentity(artifactContent),
|
|
ReleasePath: "glory-soft-yms-20260815.jar",
|
|
SlotJarPath: slotJarPath,
|
|
PreviousSlotTarget: previousTarget,
|
|
UnitName: "yms-green.service",
|
|
Port: 8081,
|
|
HealthEndpoint: "http://127.0.0.1:8081/yms/actuator/health",
|
|
}
|
|
units := &integrationUnitManager{unit: systemd.Unit{
|
|
Name: request.UnitName,
|
|
LoadState: "loaded",
|
|
ActiveState: "inactive",
|
|
SubState: "dead",
|
|
}}
|
|
healthBody := `{"status":"UP","components":{"db":{"status":"UP","components":{"dorisDataSource":{"status":"UP"},"postgresqlDataSource":{"status":"UP"}}},"diskSpace":{"status":"UP"},"ping":{"status":"UP"},"redis":{"status":"UP"}}}`
|
|
httpClient := &http.Client{Transport: integrationRoundTripFunc(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Header: make(http.Header),
|
|
Body: io.NopCloser(bytes.NewBufferString(healthBody)),
|
|
}, nil
|
|
})}
|
|
executor, err := nativebackendexecutor.New(store, coordinator, releaseStore, units, httpClient)
|
|
if err != nil {
|
|
t.Fatalf("create native backend executor: %v", err)
|
|
}
|
|
record, created, err := store.CreateTransaction(ctx, transaction.CreateRequest{
|
|
ID: "native-backend-integration",
|
|
IdempotencyKey: "native-backend-integration-request",
|
|
Source: "integration-test",
|
|
Service: "backend",
|
|
})
|
|
if err != nil || !created {
|
|
t.Fatalf("create native backend transaction: record=%+v created=%t err=%v", record, created, err)
|
|
}
|
|
|
|
if err := executor.Run(ctx, record.ID, request); err != nil {
|
|
t.Fatalf("run native backend executor: %v", err)
|
|
}
|
|
installed, found, err := releaseStore.Inspect(request.ReleasePath, request.ArtifactIdentity)
|
|
if err != nil || !found {
|
|
t.Fatalf("inspect installed backend JAR: file=%+v found=%t err=%v", installed, found, err)
|
|
}
|
|
actualContent, err := os.ReadFile(installed.Path)
|
|
if err != nil || !bytes.Equal(actualContent, artifactContent) {
|
|
t.Fatalf("verify installed backend JAR: content=%q err=%v", actualContent, err)
|
|
}
|
|
actualTarget, err := os.Readlink(slotJarPath)
|
|
if err != nil || actualTarget != installed.Path {
|
|
t.Fatalf("verify native backend slot link: target=%q installed=%q err=%v", actualTarget, installed.Path, err)
|
|
}
|
|
if err := store.Close(); err != nil {
|
|
t.Fatalf("close transaction store: %v", err)
|
|
}
|
|
|
|
reopened, err := transaction.OpenStore(ctx, databasePath)
|
|
if err != nil {
|
|
t.Fatalf("reopen transaction store: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = reopened.Close() })
|
|
persisted, err := reopened.Transaction(ctx, record.ID)
|
|
if err != nil || persisted.State != transaction.StateSwitching {
|
|
t.Fatalf("unexpected persisted native transaction: record=%+v err=%v", persisted, err)
|
|
}
|
|
pending, err := reopened.PendingSteps(ctx, record.ID)
|
|
if err != nil || len(pending) != 0 {
|
|
t.Fatalf("unexpected persisted pending steps: steps=%+v err=%v", pending, err)
|
|
}
|
|
units.mu.Lock()
|
|
startCalls := units.startCalls
|
|
startedName := units.startedName
|
|
units.mu.Unlock()
|
|
if startCalls != 1 || startedName != request.UnitName {
|
|
t.Fatalf("unexpected integrated systemd calls: start=%d name=%s", startCalls, startedName)
|
|
}
|
|
}
|
|
|
|
func integrationIdentity(content []byte) filestore.Identity {
|
|
digest := sha256.Sum256(content)
|
|
return filestore.Identity{Size: int64(len(content)), SHA256: hex.EncodeToString(digest[:])}
|
|
}
|
|
|
|
type integrationUnitManager struct {
|
|
mu sync.Mutex
|
|
unit systemd.Unit
|
|
startCalls int
|
|
stopCalls int
|
|
startedName string
|
|
}
|
|
|
|
func (m *integrationUnitManager) Inspect(_ context.Context, name string) (systemd.Unit, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if name != m.unit.Name {
|
|
return systemd.Unit{}, systemd.ErrUnitNotFound
|
|
}
|
|
return m.unit, nil
|
|
}
|
|
|
|
func (m *integrationUnitManager) Start(_ context.Context, name string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if name != m.unit.Name {
|
|
return systemd.ErrUnitNotFound
|
|
}
|
|
m.startCalls++
|
|
m.startedName = name
|
|
m.unit.ActiveState = "active"
|
|
m.unit.SubState = "running"
|
|
return nil
|
|
}
|
|
|
|
func (m *integrationUnitManager) Stop(_ context.Context, name string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if name != m.unit.Name {
|
|
return systemd.ErrUnitNotFound
|
|
}
|
|
m.stopCalls++
|
|
m.unit.ActiveState = "inactive"
|
|
m.unit.SubState = "dead"
|
|
return nil
|
|
}
|
|
|
|
type integrationRoundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (f integrationRoundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
if request.URL.String() != "http://127.0.0.1:8081/yms/actuator/health" {
|
|
return nil, errors.New("unexpected native backend health endpoint: " + request.URL.String())
|
|
}
|
|
return f(request)
|
|
}
|
|
|
|
var _ systemd.Manager = (*integrationUnitManager)(nil)
|