tenseleyflow/shithub / 670c2e9

Browse files

api/actions_lifecycle_rest: integration tests

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
670c2e9edef6ca4bd029e6edab0d93b4341a1a60
Parents
c9e34c2
Tree
c34074a

1 changed file

StatusFile+-
A internal/web/handlers/api/actions_lifecycle_rest_test.go 257 0
internal/web/handlers/api/actions_lifecycle_rest_test.goadded
@@ -0,0 +1,257 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package api_test
4
+
5
+import (
6
+	"context"
7
+	"encoding/json"
8
+	"net/http"
9
+	"net/http/httptest"
10
+	"strconv"
11
+	"strings"
12
+	"testing"
13
+	"time"
14
+
15
+	"github.com/jackc/pgx/v5/pgtype"
16
+
17
+	actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc"
18
+	"github.com/tenseleyFlow/shithub/internal/auth/audit"
19
+	"github.com/tenseleyFlow/shithub/internal/auth/pat"
20
+	"github.com/tenseleyFlow/shithub/internal/auth/throttle"
21
+	"github.com/tenseleyFlow/shithub/internal/infra/storage"
22
+	"github.com/tenseleyFlow/shithub/internal/repos"
23
+)
24
+
25
+func TestActionsLifecycle_DisableThenEnableWorkflow(t *testing.T) {
26
+	pool, router, rfs, _, _, _ := seedBranchesEnv(t, "alice")
27
+	gitDir, err := rfs.RepoPath("alice", "demo")
28
+	if err != nil {
29
+		t.Fatalf("RepoPath: %v", err)
30
+	}
31
+	seedRepoWithWorkflow(t, gitDir, map[string]string{
32
+		".shithub/workflows/ci.yml": ciYAML,
33
+	})
34
+	userID := ownerIDForAlice(t, pool)
35
+	writeToken := mintRunnerAPIPAT(t, pool, userID, string(pat.ScopeRepoWrite))
36
+	readToken := mintRunnerAPIPAT(t, pool, userID, string(pat.ScopeRepoRead))
37
+
38
+	// Disable.
39
+	req := httptest.NewRequest(http.MethodPut, "/api/v1/repos/alice/demo/actions/workflows/ci.yml/disable", nil)
40
+	req.Header.Set("Authorization", "Bearer "+writeToken)
41
+	rr := httptest.NewRecorder()
42
+	router.ServeHTTP(rr, req)
43
+	if rr.Code != http.StatusNoContent {
44
+		t.Fatalf("disable status: got %d; body=%s", rr.Code, rr.Body.String())
45
+	}
46
+
47
+	// List should show state=disabled.
48
+	req = httptest.NewRequest(http.MethodGet, "/api/v1/repos/alice/demo/actions/workflows", nil)
49
+	req.Header.Set("Authorization", "Bearer "+readToken)
50
+	rr = httptest.NewRecorder()
51
+	router.ServeHTTP(rr, req)
52
+	var listed []apiWorkflow
53
+	_ = json.Unmarshal(rr.Body.Bytes(), &listed)
54
+	if len(listed) == 0 || listed[0].State != "disabled" {
55
+		t.Errorf("list after disable: got %+v", listed)
56
+	}
57
+
58
+	// Enable.
59
+	req = httptest.NewRequest(http.MethodPut, "/api/v1/repos/alice/demo/actions/workflows/ci.yml/enable", nil)
60
+	req.Header.Set("Authorization", "Bearer "+writeToken)
61
+	rr = httptest.NewRecorder()
62
+	router.ServeHTTP(rr, req)
63
+	if rr.Code != http.StatusNoContent {
64
+		t.Fatalf("enable status: got %d; body=%s", rr.Code, rr.Body.String())
65
+	}
66
+
67
+	// List should be active again.
68
+	req = httptest.NewRequest(http.MethodGet, "/api/v1/repos/alice/demo/actions/workflows", nil)
69
+	req.Header.Set("Authorization", "Bearer "+readToken)
70
+	rr = httptest.NewRecorder()
71
+	router.ServeHTTP(rr, req)
72
+	_ = json.Unmarshal(rr.Body.Bytes(), &listed)
73
+	if len(listed) == 0 || listed[0].State != "active" {
74
+		t.Errorf("list after enable: got %+v", listed)
75
+	}
76
+}
77
+
78
+func TestActionsLifecycle_DisableRequiresRepoWrite(t *testing.T) {
79
+	_, router, _, token, _, _ := seedBranchesEnv(t, "alice")
80
+	req := httptest.NewRequest(http.MethodPut, "/api/v1/repos/alice/demo/actions/workflows/ci.yml/disable", nil)
81
+	req.Header.Set("Authorization", "Bearer "+token) // repo:read only
82
+	rr := httptest.NewRecorder()
83
+	router.ServeHTTP(rr, req)
84
+	if rr.Code != http.StatusForbidden {
85
+		t.Fatalf("status: got %d, want 403; body=%s", rr.Code, rr.Body.String())
86
+	}
87
+}
88
+
89
+func TestActionsLifecycle_RunDelete(t *testing.T) {
90
+	pool, router, userID, repoID, _ := seedIssuesEnv(t, "alice")
91
+	run := seedWorkflowRun(t, pool, repoID, userID, 1, ".shithub/workflows/ci.yml")
92
+	writeToken := mintRunnerAPIPAT(t, pool, userID, string(pat.ScopeRepoWrite))
93
+
94
+	req := httptest.NewRequest(http.MethodDelete, "/api/v1/repos/alice/demo/actions/runs/"+strconv.FormatInt(run.ID, 10), nil)
95
+	req.Header.Set("Authorization", "Bearer "+writeToken)
96
+	rr := httptest.NewRecorder()
97
+	router.ServeHTTP(rr, req)
98
+	if rr.Code != http.StatusNoContent {
99
+		t.Fatalf("status: got %d; body=%s", rr.Code, rr.Body.String())
100
+	}
101
+	if _, err := actionsdb.New().GetWorkflowRunByID(context.Background(), pool, run.ID); err == nil {
102
+		t.Errorf("run still present after delete")
103
+	}
104
+}
105
+
106
+func TestActionsLifecycle_RunDeleteUnknown404(t *testing.T) {
107
+	pool, router, userID, _, _ := seedIssuesEnv(t, "alice")
108
+	writeToken := mintRunnerAPIPAT(t, pool, userID, string(pat.ScopeRepoWrite))
109
+
110
+	req := httptest.NewRequest(http.MethodDelete, "/api/v1/repos/alice/demo/actions/runs/99999", nil)
111
+	req.Header.Set("Authorization", "Bearer "+writeToken)
112
+	rr := httptest.NewRecorder()
113
+	router.ServeHTTP(rr, req)
114
+	if rr.Code != http.StatusNotFound {
115
+		t.Fatalf("status: got %d, want 404; body=%s", rr.Code, rr.Body.String())
116
+	}
117
+}
118
+
119
+func TestActionsLifecycle_ArtifactsListAndGet(t *testing.T) {
120
+	pool, router, userID, repoID, token := seedIssuesEnv(t, "alice")
121
+	run := seedWorkflowRun(t, pool, repoID, userID, 1, ".shithub/workflows/ci.yml")
122
+	art, err := actionsdb.New().InsertArtifact(context.Background(), pool, actionsdb.InsertArtifactParams{
123
+		RunID:     run.ID,
124
+		Name:      "logs",
125
+		ObjectKey: "actions/runs/1/logs.zip",
126
+		ByteCount: 42,
127
+		ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(48 * time.Hour), Valid: true},
128
+	})
129
+	if err != nil {
130
+		t.Fatalf("InsertArtifact: %v", err)
131
+	}
132
+
133
+	req := httptest.NewRequest(http.MethodGet, "/api/v1/repos/alice/demo/actions/runs/"+strconv.FormatInt(run.ID, 10)+"/artifacts", nil)
134
+	req.Header.Set("Authorization", "Bearer "+token)
135
+	rr := httptest.NewRecorder()
136
+	router.ServeHTTP(rr, req)
137
+	if rr.Code != http.StatusOK {
138
+		t.Fatalf("list status: got %d; body=%s", rr.Code, rr.Body.String())
139
+	}
140
+	var listed []map[string]any
141
+	_ = json.Unmarshal(rr.Body.Bytes(), &listed)
142
+	if len(listed) != 1 {
143
+		t.Fatalf("expected 1 artifact; got %+v", listed)
144
+	}
145
+	if name, _ := listed[0]["name"].(string); name != "logs" {
146
+		t.Errorf("name: %+v", listed[0])
147
+	}
148
+	if archive, _ := listed[0]["archive_url"].(string); !strings.Contains(archive, "/actions/artifacts/") {
149
+		t.Errorf("archive_url: %+v", listed[0])
150
+	}
151
+
152
+	req = httptest.NewRequest(http.MethodGet, "/api/v1/repos/alice/demo/actions/artifacts/"+strconv.FormatInt(art.ID, 10), nil)
153
+	req.Header.Set("Authorization", "Bearer "+token)
154
+	rr = httptest.NewRecorder()
155
+	router.ServeHTTP(rr, req)
156
+	if rr.Code != http.StatusOK {
157
+		t.Fatalf("get status: got %d; body=%s", rr.Code, rr.Body.String())
158
+	}
159
+}
160
+
161
+func TestActionsLifecycle_ArtifactCrossRepo404(t *testing.T) {
162
+	pool, router, userID, repoID, _ := seedIssuesEnv(t, "alice")
163
+	run := seedWorkflowRun(t, pool, repoID, userID, 1, ".shithub/workflows/ci.yml")
164
+	art, err := actionsdb.New().InsertArtifact(context.Background(), pool, actionsdb.InsertArtifactParams{
165
+		RunID:     run.ID,
166
+		Name:      "logs",
167
+		ObjectKey: "actions/runs/1/logs.zip",
168
+		ByteCount: 42,
169
+		ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(48 * time.Hour), Valid: true},
170
+	})
171
+	if err != nil {
172
+		t.Fatalf("InsertArtifact: %v", err)
173
+	}
174
+
175
+	bobID := seedRepoCreatorUser(t, pool, "bob")
176
+	bobToken := mintRunnerAPIPAT(t, pool, bobID, string(pat.ScopeRepoRead))
177
+	// Create a separate fork repo owned by bob using the repos orchestrator.
178
+	rfs, rerr := storage.NewRepoFS(t.TempDir())
179
+	if rerr != nil {
180
+		t.Fatalf("NewRepoFS: %v", rerr)
181
+	}
182
+	if _, err := repos.Create(context.Background(), repos.Deps{
183
+		Pool:    pool,
184
+		RepoFS:  rfs,
185
+		Audit:   audit.NewRecorder(),
186
+		Limiter: throttle.NewLimiter(),
187
+	}, repos.Params{
188
+		ActorUserID:   bobID,
189
+		OwnerUserID:   bobID,
190
+		OwnerUsername: "bob",
191
+		Name:          "fork",
192
+		Description:   "bob fork",
193
+		Visibility:    "public",
194
+	}); err != nil {
195
+		t.Fatalf("repos.Create: %v", err)
196
+	}
197
+
198
+	req := httptest.NewRequest(http.MethodGet, "/api/v1/repos/bob/fork/actions/artifacts/"+strconv.FormatInt(art.ID, 10), nil)
199
+	req.Header.Set("Authorization", "Bearer "+bobToken)
200
+	rr := httptest.NewRecorder()
201
+	router.ServeHTTP(rr, req)
202
+	if rr.Code != http.StatusNotFound {
203
+		t.Fatalf("status: got %d, want 404; body=%s", rr.Code, rr.Body.String())
204
+	}
205
+}
206
+
207
+func TestActionsLifecycle_JobLogsAssemble(t *testing.T) {
208
+	pool, router, userID, repoID, token := seedIssuesEnv(t, "alice")
209
+	run := seedWorkflowRun(t, pool, repoID, userID, 1, ".shithub/workflows/ci.yml")
210
+	q := actionsdb.New()
211
+	job, err := q.InsertWorkflowJob(context.Background(), pool, actionsdb.InsertWorkflowJobParams{
212
+		RunID:          run.ID,
213
+		JobIndex:       0,
214
+		JobKey:         "build",
215
+		JobName:        "Build",
216
+		RunsOn:         "ubuntu-latest",
217
+		NeedsJobs:      []string{},
218
+		TimeoutMinutes: 60,
219
+		Permissions:    []byte(`{}`),
220
+		JobEnv:         []byte(`{}`),
221
+	})
222
+	if err != nil {
223
+		t.Fatalf("InsertWorkflowJob: %v", err)
224
+	}
225
+	step, err := q.InsertWorkflowStep(context.Background(), pool, actionsdb.InsertWorkflowStepParams{
226
+		JobID:      job.ID,
227
+		StepIndex:  0,
228
+		StepID:     "checkout",
229
+		StepName:   "Check out",
230
+		RunCommand: "",
231
+		UsesAlias:  "actions/checkout@v4",
232
+		StepEnv:    []byte(`{}`),
233
+		StepWith:   []byte(`{}`),
234
+	})
235
+	if err != nil {
236
+		t.Fatalf("InsertWorkflowStep: %v", err)
237
+	}
238
+	if _, err := q.AppendStepLogChunk(context.Background(), pool, actionsdb.AppendStepLogChunkParams{
239
+		StepID: step.ID, Seq: 1, Chunk: []byte("hello\nworld\n"),
240
+	}); err != nil {
241
+		t.Fatalf("AppendStepLogChunk: %v", err)
242
+	}
243
+
244
+	req := httptest.NewRequest(http.MethodGet, "/api/v1/repos/alice/demo/actions/jobs/"+strconv.FormatInt(job.ID, 10)+"/logs", nil)
245
+	req.Header.Set("Authorization", "Bearer "+token)
246
+	rr := httptest.NewRecorder()
247
+	router.ServeHTTP(rr, req)
248
+	if rr.Code != http.StatusOK {
249
+		t.Fatalf("status: got %d; body=%s", rr.Code, rr.Body.String())
250
+	}
251
+	if !strings.Contains(rr.Body.String(), "hello\nworld") {
252
+		t.Errorf("log body missing payload: %s", rr.Body.String())
253
+	}
254
+	if !strings.Contains(rr.Body.String(), "step 0: Check out") {
255
+		t.Errorf("step header missing: %s", rr.Body.String())
256
+	}
257
+}