v3.0.0-alpha.1 6fdddb8

app_deploy_handler_lifecycle_test.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"encoding/json"
6	"net/http"
7	"net/http/httptest"
8	"sync"
9	"testing"
10	"time"
11
12	"github.com/bnema/zerowrap"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/mock"
15	"github.com/stretchr/testify/require"
16
17	"github.com/bnema/gordon/internal/adapters/dto"
18	"github.com/bnema/gordon/internal/adapters/in/http/admin"
19	outmocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
20	"github.com/bnema/gordon/internal/domain"
21	"github.com/bnema/gordon/internal/usecase/apps"
22	"github.com/bnema/gordon/internal/usecase/deployment"
23)
24
25// blockingHandlerDeployEngine implements the deployment-engine port the
26// daemon-owned app service executes through. ExecuteDeploy reports the
27// context it received and blocks until released, so request-cancellation
28// independence is observable at the HTTP handler boundary.
29type blockingHandlerDeployEngine struct {
30	started chan context.Context
31	release chan struct{}
32}
33
34func (e *blockingHandlerDeployEngine) StartDeploy(context.Context, deployment.DeployInput) (*deployment.StartDeployResult, error) {
35	op := domain.AppOperation{Op: "op-1", Kind: "deploy", App: "blog", InputRevision: "rev-1"}
36	return &deployment.StartDeployResult{
37		Claim: deployment.DeployClaim{App: "blog", Op: "op-1", Service: "web", Revision: "rev-1", Journal: op},
38		Owned: true,
39	}, nil
40}
41
42func (e *blockingHandlerDeployEngine) ExecuteDeploy(ctx context.Context, _ deployment.DeployClaim) (*deployment.DeployResult, error) {
43	e.started <- ctx
44	<-e.release
45	return &deployment.DeployResult{Op: "op-1", App: "blog"}, nil
46}
47
48func (e *blockingHandlerDeployEngine) AbandonDeploy(context.Context, deployment.DeployClaim) error {
49	return nil
50}
51
52func (e *blockingHandlerDeployEngine) Stop(context.Context, string, string) (*deployment.LifecycleResult, error) {
53	return nil, nil
54}
55
56func (e *blockingHandlerDeployEngine) Start(context.Context, string, string) (*deployment.LifecycleResult, error) {
57	return nil, nil
58}
59
60func (e *blockingHandlerDeployEngine) Restart(context.Context, string, string, string) (*deployment.LifecycleResult, error) {
61	return nil, nil
62}
63
64func (e *blockingHandlerDeployEngine) Remove(context.Context, string, string) (*deployment.LifecycleResult, error) {
65	return nil, nil
66}
67
68// TestAppDeployHandler_RequestCancellationDoesNotAbortExecution drives the
69// admin deploy handler over the real daemon-owned AppServiceImpl and proves
70// the request boundary is inert: the handler answers 202 with the persisted
71// running journal promptly, and cancelling the request context afterwards
72// never reaches the in-flight replacement. The handler owns no goroutine; the
73// execution derives from the daemon context.
74func TestAppDeployHandler_RequestCancellationDoesNotAbortExecution(t *testing.T) {
75	store := outmocks.NewMockAppState(t)
76	running := domain.AppOperation{Op: "op-1", Kind: "deploy", App: "blog", InputRevision: "rev-1"}
77	store.EXPECT().LoadOperation(mock.Anything, "blog", "op-1").Return(running, nil).Once()
78	// Show resolves no app: the response carries the journal alone.
79	store.EXPECT().AppExists(mock.Anything, "blog").Return(false, nil).Once()
80
81	release := make(chan struct{})
82	var releaseOnce sync.Once
83	unblock := func() { releaseOnce.Do(func() { close(release) }) }
84
85	engine := &blockingHandlerDeployEngine{started: make(chan context.Context, 1), release: release}
86	svc := apps.NewAppServiceImpl(store, engine, nil, zerowrap.Default()).WithDaemonContext(context.Background())
87	t.Cleanup(func() {
88		unblock()
89		_ = svc.Shutdown(context.Background())
90	})
91
92	handler := admin.NewHandler(admin.HandlerDeps{Log: zerowrap.Default(), AppSvc: svc})
93
94	reqCtx, cancelRequest := context.WithCancel(
95		context.WithValue(context.Background(), domain.ContextKeyScopes, []string{"admin:apps:write"}))
96	defer cancelRequest()
97	req := httptest.NewRequest(http.MethodPost, "/admin/apps/blog/deploy", nil)
98	req.Header.Set("Idempotency-Key", "key-1")
99	req = req.WithContext(reqCtx)
100	rec := httptest.NewRecorder()
101
102	serveDone := make(chan struct{})
103	go func() {
104		defer close(serveDone)
105		handler.ServeHTTP(rec, req)
106	}()
107
108	var execCtx context.Context
109	select {
110	case execCtx = <-engine.started:
111	case <-time.After(3 * time.Second):
112		t.Fatal("background deploy execution never started")
113	}
114	select {
115	case <-serveDone:
116	case <-time.After(3 * time.Second):
117		t.Fatal("handler did not answer promptly")
118	}
119	require.Equal(t, http.StatusAccepted, rec.Code)
120	// The persisted journal is what the handler answers with: the client can
121	// poll the same op through operations/by-key.
122	var resp dto.AppDeployResponse
123	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
124	assert.Equal(t, "op-1", resp.Op)
125	assert.Equal(t, dto.AppStatusRunning, resp.Status)
126
127	// The request is over and its context is cancelled: the daemon-owned
128	// execution must be unaffected.
129	cancelRequest()
130	select {
131	case <-execCtx.Done():
132		t.Fatal("request cancellation aborted the daemon-owned execution")
133	case <-time.After(50 * time.Millisecond):
134	}
135}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close