v3.4.1 de8f68e

prune_test.go RawHistoryFolder

1package admin
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"net/http"
8	"net/http/httptest"
9	"testing"
10
11	"github.com/stretchr/testify/assert"
12	"github.com/stretchr/testify/mock"
13	"github.com/stretchr/testify/require"
14
15	"github.com/bnema/gordon/internal/adapters/dto"
16	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
17	"github.com/bnema/gordon/internal/domain"
18)
19
20// TestHandler_ImagesPrune_ExposesPerCandidateVerdicts proves the wire
21// response carries the same plan shape for a dry run and an execution,
22// including the reasons protected and unknown candidates were skipped.
23func TestHandler_ImagesPrune_ExposesPerCandidateVerdicts(t *testing.T) {
24	plan := domain.PruneReport{
25		Applied: false,
26		Candidates: []domain.PruneCandidateReport{
27			{Kind: domain.PruneResourceRegistryTag, Ref: "app:v1", Verdict: domain.PruneVerdictEligible,
28				Reasons: []domain.PruneReason{domain.PruneReasonEligibleRetention}},
29			{Kind: domain.PruneResourceRuntimeImage, Ref: "sha256:active", Verdict: domain.PruneVerdictProtected,
30				Reasons: []domain.PruneReason{domain.PruneReasonProtectedContainerUse}},
31			{Kind: domain.PruneResourceRuntimeImage, Ref: "sha256:opaque", Verdict: domain.PruneVerdictUnknown,
32				Reasons: []domain.PruneReason{domain.PruneReasonUnknownProvenance}},
33		},
34		Gaps: []domain.InventoryGap{{
35			Source: domain.InventorySourceRuntimeContainers,
36			Reason: domain.PruneReasonUnknownContainerUse,
37			Detail: "list containers failed",
38		}},
39		Failures: []domain.PruneFailure{{
40			Kind: domain.PruneResourceRegistryTag, Ref: "app:v9", Err: "storage unavailable",
41		}},
42	}
43
44	var dryRunSeen bool
45	imageSvc := &stubImageService{
46		pruneFunc: func(_ context.Context, opts domain.ImagePruneOptions) (domain.ImagePruneReport, error) {
47			dryRunSeen = opts.DryRun
48			return domain.ImagePruneReport{Plan: plan}, nil
49		},
50	}
51
52	handler := newTestHandler(t, func(d *HandlerDeps) {
53		d.ImageSvc = imageSvc
54	})
55
56	rec := httptest.NewRecorder()
57	req := httptest.NewRequest(http.MethodPost, "/admin/images/prune", bytes.NewBufferString(`{"dry_run": true}`))
58	req = req.WithContext(ctxWithScopes("admin:config:write"))
59	handler.ServeHTTP(rec, req)
60
61	require.Equal(t, http.StatusOK, rec.Code)
62	assert.True(t, dryRunSeen, "the dry-run flag must reach the use case")
63
64	var resp dto.ImagePruneResponse
65	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
66	assert.False(t, resp.Plan.Applied)
67	assert.Equal(t, 1, resp.Plan.Eligible)
68	assert.Equal(t, 1, resp.Plan.Protected)
69	assert.Equal(t, 1, resp.Plan.Unknown)
70	require.Len(t, resp.Plan.Candidates, 3)
71	assert.Equal(t, "protected-container-use", resp.Plan.Candidates[1].Reasons[0])
72	require.Len(t, resp.Plan.Failures, 1)
73	assert.Equal(t, "storage unavailable", resp.Plan.Failures[0].Error)
74	require.Len(t, resp.Plan.Gaps, 1)
75	assert.Equal(t, "runtime-containers", resp.Plan.Gaps[0].Source)
76}
77
78// TestHandler_VolumesPrune_ExposesPerCandidateVerdicts proves the volume
79// endpoint returns the same shared plan shape, including a zero-deletion
80// success.
81func TestHandler_VolumesPrune_ExposesPerCandidateVerdicts(t *testing.T) {
82	report := &domain.VolumePruneReport{
83		Plan: domain.PruneReport{
84			Applied: true,
85			Candidates: []domain.PruneCandidateReport{
86				{Kind: domain.PruneResourceVolume, Ref: "gordon-shop--web--vol--data", Verdict: domain.PruneVerdictProtected,
87					Reasons: []domain.PruneReason{domain.PruneReasonProtectedOwnership}},
88			},
89		},
90	}
91
92	volumeSvc := inmocks.NewMockVolumeService(t)
93	volumeSvc.EXPECT().PruneVolumes(mock.Anything, false).Return(report, []*domain.VolumeInfo{}, nil).Once()
94	handler := newTestHandler(t, func(d *HandlerDeps) {
95		d.VolumeSvc = volumeSvc
96	})
97
98	rec := httptest.NewRecorder()
99	req := httptest.NewRequest(http.MethodPost, "/admin/volumes/prune", bytes.NewBufferString(`{"dry_run": false}`))
100	req = req.WithContext(ctxWithScopes("admin:volumes:write"))
101	handler.ServeHTTP(rec, req)
102
103	require.Equal(t, http.StatusOK, rec.Code)
104	var resp dto.VolumePruneResponse
105	require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &resp))
106	assert.True(t, resp.Plan.Applied)
107	assert.Zero(t, resp.VolumesRemoved)
108	assert.Equal(t, 1, resp.Plan.Protected)
109	assert.Zero(t, resp.Plan.Eligible, "a protected candidate is not eligible")
110	require.Len(t, resp.Plan.Candidates, 1)
111	assert.Equal(t, "protected-ownership", resp.Plan.Candidates[0].Reasons[0])
112}

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