mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-17 15:19:00 +02:00
* feat(autopilot): add View/Write permission layer Autopilot write and execute operations were gated only by workspace membership, so any member could edit, delete, trigger, or rotate the webhook of any autopilot, and GetAutopilot returned webhook tokens to every member (a token alone can trigger the autopilot). - Add canWriteAutopilot / requireAutopilotWrite: update, delete, trigger, replay-delivery, and all trigger/secret management now require the autopilot creator or a workspace owner/admin. - Redact webhook_token/path/url in GetAutopilot for callers without write access; trigger metadata otherwise stays visible (View default = all members). Creating an autopilot stays open to any member. - ANDs with the existing private-assignee-agent dispatch gate. MUL-3807 Co-authored-by: multica-agent <github@multica.ai> * feat(autopilot): delegate write access via collaborators + manage-access UI Adds an explicit grant primitive so an autopilot's creator/admin can authorize specific workspace members to manage it, with a frontend entry point — beyond the implicit creator/owner-admin set from the prior commit. Backend: - New autopilot_collaborator table (migration 128, members-only, app-layer cleanup, no FK) + sqlc queries. - memberCanWriteAutopilot now also honors explicit collaborators; the write gate, webhook-secret redaction, and a new per-caller can_write flag (on list + detail) all flow through it. - POST/DELETE /api/autopilots/{id}/collaborators (writer-gated); GetAutopilot embeds the collaborators list. Delete cleans up grants in its transaction. - Tests: grant->write->revoke flow, non-writer can't grant, non-member rejected. Frontend (web + desktop via packages/views): - ManageAccessDialog: member picker to grant/revoke, current list with remove. - 'Manage access' entry in the autopilot detail header; edit/run/add-trigger/ delete and the list-row kebab + per-trigger rotate/delete now gate on can_write (absent => allowed, server stays the gate). - can_write wired through types/schema/api client/mutations; en + zh-Hans copy. MUL-3807 Co-authored-by: multica-agent <github@multica.ai> * fix(autopilot): add manage-access i18n keys to ja/ko locales The locale parity test requires every non-EN bundle to cover every EN key. The prior commit added detail.manage_access + the access.* block to en and zh-Hans only, failing parity for ja and ko. Add the translated keys to both. Co-authored-by: multica-agent <github@multica.ai> * fix(autopilot): restrict access-list management to creator/admin only Final-review fix: AddAutopilotCollaborator/RemoveAutopilotCollaborator used requireAutopilotWrite, which counts granted collaborators as writers — so a collaborator could in turn grant/revoke others, a privilege escalation contradicting the 'collaborators cannot re-grant' design. - New requireAutopilotAccessManagement guard uses the narrower autopilotWriteByOwnership predicate (creator or workspace owner/admin only); swapped into both collaborator endpoints. Collaborators keep their edit/trigger/secret write-execute rights. - GetAutopilot now also stamps can_manage_access (narrower than can_write); the detail page gates the 'Manage access' button on it so collaborators no longer see an entry that would 403. - Tests: collaborator grant-others -> 403, revoke-peer -> 403, while retaining edit; can_manage_access true for owner, false for collaborator. MUL-3807 Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: J <j@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
424 lines
16 KiB
Go
424 lines
16 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// createPlainMember adds a fresh member-role user to the test workspace and
|
|
// returns its user id. Used to exercise the autopilot write gate from the
|
|
// perspective of a member who is neither the creator nor a workspace admin.
|
|
func createPlainMember(t *testing.T, email string) string {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
|
|
var userID string
|
|
if err := testPool.QueryRow(ctx,
|
|
`INSERT INTO "user" (name, email) VALUES ('AP Perm Member', $1) RETURNING id`,
|
|
email,
|
|
).Scan(&userID); err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
testPool.Exec(context.Background(), `DELETE FROM member WHERE workspace_id = $1 AND user_id = $2`, testWorkspaceID, userID)
|
|
testPool.Exec(context.Background(), `DELETE FROM "user" WHERE id = $1`, userID)
|
|
})
|
|
|
|
if _, err := testPool.Exec(ctx,
|
|
`INSERT INTO member (workspace_id, user_id, role) VALUES ($1, $2, 'member')`,
|
|
testWorkspaceID, userID,
|
|
); err != nil {
|
|
t.Fatalf("add member: %v", err)
|
|
}
|
|
return userID
|
|
}
|
|
|
|
// createAutopilotAs creates an autopilot via the API as the given user (empty
|
|
// userID = workspace owner) assigned to a fresh public agent, and returns its
|
|
// id. The caller-supplied title prefix keeps cleanup queries unambiguous.
|
|
func createAutopilotAs(t *testing.T, userID, title string) string {
|
|
t.Helper()
|
|
agentID := createHandlerTestAgent(t, title+"-agent", nil)
|
|
|
|
body := map[string]any{
|
|
"title": title,
|
|
"assignee_id": agentID,
|
|
"execution_mode": "create_issue",
|
|
}
|
|
w := httptest.NewRecorder()
|
|
path := "/api/autopilots?workspace_id=" + testWorkspaceID
|
|
var r *http.Request
|
|
if userID == "" {
|
|
r = newRequest("POST", path, body)
|
|
} else {
|
|
r = newRequestAs(userID, "POST", path, body)
|
|
}
|
|
testHandler.CreateAutopilot(w, r)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("CreateAutopilot: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var ap AutopilotResponse
|
|
if err := json.NewDecoder(w.Body).Decode(&ap); err != nil {
|
|
t.Fatalf("decode autopilot: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
testPool.Exec(context.Background(), `DELETE FROM autopilot_run WHERE autopilot_id = $1`, ap.ID)
|
|
testPool.Exec(context.Background(), `DELETE FROM autopilot_trigger WHERE autopilot_id = $1`, ap.ID)
|
|
testPool.Exec(context.Background(), `DELETE FROM autopilot_collaborator WHERE autopilot_id = $1`, ap.ID)
|
|
testPool.Exec(context.Background(), `DELETE FROM autopilot WHERE id = $1`, ap.ID)
|
|
})
|
|
return ap.ID
|
|
}
|
|
|
|
// grantAutopilotAccess grants the target member write access via the API as the
|
|
// given caller (empty caller = workspace owner), asserting the expected status.
|
|
func grantAutopilotAccess(t *testing.T, caller, apID, targetUserID string, wantStatus int) {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
path := "/api/autopilots/" + apID + "/collaborators?workspace_id=" + testWorkspaceID
|
|
body := map[string]any{"user_id": targetUserID}
|
|
var r *http.Request
|
|
if caller == "" {
|
|
r = newRequest("POST", path, body)
|
|
} else {
|
|
r = newRequestAs(caller, "POST", path, body)
|
|
}
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.AddAutopilotCollaborator(w, r)
|
|
if w.Code != wantStatus {
|
|
t.Fatalf("AddAutopilotCollaborator: expected %d, got %d: %s", wantStatus, w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// autopilotCanWrite fetches the detail as the given caller and returns the
|
|
// can_write flag the server stamped for them.
|
|
func autopilotCanWrite(t *testing.T, caller, apID string) bool {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
path := "/api/autopilots/" + apID + "?workspace_id=" + testWorkspaceID
|
|
var r *http.Request
|
|
if caller == "" {
|
|
r = newRequest("GET", path, nil)
|
|
} else {
|
|
r = newRequestAs(caller, "GET", path, nil)
|
|
}
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.GetAutopilot(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GetAutopilot: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var got struct {
|
|
Autopilot AutopilotResponse `json:"autopilot"`
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
|
|
t.Fatalf("decode autopilot: %v", err)
|
|
}
|
|
if got.Autopilot.CanWrite == nil {
|
|
t.Fatalf("expected can_write to be set on detail response")
|
|
}
|
|
return *got.Autopilot.CanWrite
|
|
}
|
|
|
|
// autopilotCanManageAccess fetches the detail as the given caller and returns
|
|
// the can_manage_access flag (narrower than can_write — collaborators lack it).
|
|
func autopilotCanManageAccess(t *testing.T, caller, apID string) bool {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
path := "/api/autopilots/" + apID + "?workspace_id=" + testWorkspaceID
|
|
var r *http.Request
|
|
if caller == "" {
|
|
r = newRequest("GET", path, nil)
|
|
} else {
|
|
r = newRequestAs(caller, "GET", path, nil)
|
|
}
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.GetAutopilot(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GetAutopilot: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var got struct {
|
|
Autopilot AutopilotResponse `json:"autopilot"`
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&got); err != nil {
|
|
t.Fatalf("decode autopilot: %v", err)
|
|
}
|
|
if got.Autopilot.CanManageAccess == nil {
|
|
t.Fatalf("expected can_manage_access to be set on detail response")
|
|
}
|
|
return *got.Autopilot.CanManageAccess
|
|
}
|
|
|
|
// TestAutopilotCollaborator_GrantedMemberCanWrite verifies the full delegation
|
|
// flow: a non-creator member is blocked, becomes a writer once granted, and is
|
|
// blocked again after the grant is revoked (MUL-3807).
|
|
func TestAutopilotCollaborator_GrantedMemberCanWrite(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
apID := createAutopilotAs(t, "", "ap-collab-grant")
|
|
member := createPlainMember(t, "ap-collab-grantee@multica.test")
|
|
|
|
updateAs := func(caller string) int {
|
|
w := httptest.NewRecorder()
|
|
r := newRequestAs(caller, "PATCH", "/api/autopilots/"+apID+"?workspace_id="+testWorkspaceID, map[string]any{"title": "edited by " + caller})
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.UpdateAutopilot(w, r)
|
|
return w.Code
|
|
}
|
|
|
|
// Before grant: blocked, and can_write=false.
|
|
if code := updateAs(member); code != http.StatusForbidden {
|
|
t.Fatalf("pre-grant update: expected 403, got %d", code)
|
|
}
|
|
if autopilotCanWrite(t, member, apID) {
|
|
t.Fatalf("pre-grant: expected can_write=false for member")
|
|
}
|
|
|
|
// Grant.
|
|
grantAutopilotAccess(t, "", apID, member, http.StatusCreated)
|
|
|
|
// After grant: allowed, and can_write=true.
|
|
if !autopilotCanWrite(t, member, apID) {
|
|
t.Fatalf("post-grant: expected can_write=true for collaborator")
|
|
}
|
|
if code := updateAs(member); code != http.StatusOK {
|
|
t.Fatalf("post-grant update: expected 200, got %d", code)
|
|
}
|
|
|
|
// Revoke.
|
|
w := httptest.NewRecorder()
|
|
r := newRequest("DELETE", "/api/autopilots/"+apID+"/collaborators/"+member+"?workspace_id="+testWorkspaceID, nil)
|
|
r = withURLParams(r, "id", apID, "userId", member)
|
|
testHandler.RemoveAutopilotCollaborator(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("RemoveAutopilotCollaborator: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// After revoke: blocked again.
|
|
if code := updateAs(member); code != http.StatusForbidden {
|
|
t.Fatalf("post-revoke update: expected 403, got %d", code)
|
|
}
|
|
}
|
|
|
|
// TestAutopilotCollaborator_NonWriterCannotGrant verifies a member without write
|
|
// access cannot manage the access list, and granting a non-member is rejected.
|
|
func TestAutopilotCollaborator_NonWriterCannotGrant(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
apID := createAutopilotAs(t, "", "ap-collab-guard")
|
|
stranger := createPlainMember(t, "ap-collab-stranger@multica.test")
|
|
victim := createPlainMember(t, "ap-collab-victim@multica.test")
|
|
|
|
// A non-writer cannot grant access to anyone.
|
|
grantAutopilotAccess(t, stranger, apID, victim, http.StatusForbidden)
|
|
|
|
// Owner granting a non-member (random UUID) is rejected as bad input.
|
|
grantAutopilotAccess(t, "", apID, "00000000-0000-0000-0000-000000000000", http.StatusBadRequest)
|
|
}
|
|
|
|
// TestAutopilotCollaborator_CannotManageAccessList verifies the privilege-
|
|
// escalation boundary: a granted collaborator keeps write/execute access but
|
|
// CANNOT manage the access list — they cannot grant access to others or revoke
|
|
// peers. Only the creator / owner / admin may manage access (MUL-3807).
|
|
func TestAutopilotCollaborator_CannotManageAccessList(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
apID := createAutopilotAs(t, "", "ap-collab-noescalate")
|
|
carol := createPlainMember(t, "ap-collab-carol@multica.test")
|
|
dave := createPlainMember(t, "ap-collab-dave@multica.test")
|
|
bob := createPlainMember(t, "ap-collab-bob2@multica.test")
|
|
|
|
// Owner grants two collaborators.
|
|
grantAutopilotAccess(t, "", apID, carol, http.StatusCreated)
|
|
grantAutopilotAccess(t, "", apID, dave, http.StatusCreated)
|
|
|
|
// Collaborator carol keeps write access (can still edit the autopilot).
|
|
w := httptest.NewRecorder()
|
|
r := newRequestAs(carol, "PATCH", "/api/autopilots/"+apID+"?workspace_id="+testWorkspaceID, map[string]any{"title": "carol edit"})
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.UpdateAutopilot(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("collaborator update: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// ...but cannot grant access to a new member.
|
|
grantAutopilotAccess(t, carol, apID, bob, http.StatusForbidden)
|
|
|
|
// ...and cannot revoke a peer collaborator.
|
|
w = httptest.NewRecorder()
|
|
r = newRequestAs(carol, "DELETE", "/api/autopilots/"+apID+"/collaborators/"+dave+"?workspace_id="+testWorkspaceID, nil)
|
|
r = withURLParams(r, "id", apID, "userId", dave)
|
|
testHandler.RemoveAutopilotCollaborator(w, r)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("collaborator revoke peer: expected 403, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// can_manage_access: false for the collaborator, true for the owner.
|
|
if autopilotCanManageAccess(t, carol, apID) {
|
|
t.Fatalf("carol can_manage_access: expected false")
|
|
}
|
|
if !autopilotCanManageAccess(t, "", apID) {
|
|
t.Fatalf("owner can_manage_access: expected true")
|
|
}
|
|
}
|
|
|
|
// TestAutopilotWrite_PlainMemberCannotMutateOthers verifies that a workspace
|
|
// member who is neither the creator nor an admin cannot edit, trigger, or
|
|
// delete an autopilot created by someone else (MUL-3807).
|
|
func TestAutopilotWrite_PlainMemberCannotMutateOthers(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
apID := createAutopilotAs(t, "", "ap-perm-owner-created")
|
|
member := createPlainMember(t, "ap-perm-stranger@multica.test")
|
|
|
|
// Update.
|
|
w := httptest.NewRecorder()
|
|
r := newRequestAs(member, "PATCH", "/api/autopilots/"+apID+"?workspace_id="+testWorkspaceID, map[string]any{"title": "hijacked"})
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.UpdateAutopilot(w, r)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("UpdateAutopilot by stranger: expected 403, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Trigger.
|
|
w = httptest.NewRecorder()
|
|
r = newRequestAs(member, "POST", "/api/autopilots/"+apID+"/trigger?workspace_id="+testWorkspaceID, nil)
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.TriggerAutopilot(w, r)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("TriggerAutopilot by stranger: expected 403, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Delete.
|
|
w = httptest.NewRecorder()
|
|
r = newRequestAs(member, "DELETE", "/api/autopilots/"+apID+"?workspace_id="+testWorkspaceID, nil)
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.DeleteAutopilot(w, r)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Fatalf("DeleteAutopilot by stranger: expected 403, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAutopilotWrite_CreatorCanMutateOwn verifies that the member who created
|
|
// an autopilot retains write access to it even without an admin role.
|
|
func TestAutopilotWrite_CreatorCanMutateOwn(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
member := createPlainMember(t, "ap-perm-creator@multica.test")
|
|
apID := createAutopilotAs(t, member, "ap-perm-member-created")
|
|
|
|
w := httptest.NewRecorder()
|
|
r := newRequestAs(member, "PATCH", "/api/autopilots/"+apID+"?workspace_id="+testWorkspaceID, map[string]any{"title": "creator edit"})
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.UpdateAutopilot(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("UpdateAutopilot by creator: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAutopilotWrite_AdminCanMutateMembersAutopilot verifies that a workspace
|
|
// owner/admin can manage an autopilot created by a plain member.
|
|
func TestAutopilotWrite_AdminCanMutateMembersAutopilot(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
member := createPlainMember(t, "ap-perm-admin-target@multica.test")
|
|
apID := createAutopilotAs(t, member, "ap-perm-admin-target")
|
|
|
|
// testUserID is the workspace owner.
|
|
w := httptest.NewRecorder()
|
|
r := newRequest("PATCH", "/api/autopilots/"+apID+"?workspace_id="+testWorkspaceID, map[string]any{"title": "admin edit"})
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.UpdateAutopilot(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("UpdateAutopilot by owner: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// TestAutopilotWrite_WebhookSecretRedactedForNonWriter verifies that the
|
|
// webhook token/path are returned to a writer (the owner) but stripped from
|
|
// the read response for a member who lacks write access — seeing the token is
|
|
// equivalent to being able to trigger the autopilot (MUL-3807).
|
|
func TestAutopilotWrite_WebhookSecretRedactedForNonWriter(t *testing.T) {
|
|
if testHandler == nil || testPool == nil {
|
|
t.Skip("database not available")
|
|
}
|
|
|
|
apID := createAutopilotAs(t, "", "ap-perm-secret")
|
|
stranger := createPlainMember(t, "ap-perm-secret-stranger@multica.test")
|
|
|
|
// Owner adds a webhook trigger.
|
|
w := httptest.NewRecorder()
|
|
r := newRequest("POST", "/api/autopilots/"+apID+"/triggers?workspace_id="+testWorkspaceID, map[string]any{"kind": "webhook"})
|
|
r = withURLParam(r, "id", apID)
|
|
testHandler.CreateAutopilotTrigger(w, r)
|
|
if w.Code != http.StatusCreated {
|
|
t.Fatalf("CreateAutopilotTrigger: expected 201, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
type getResp struct {
|
|
Triggers []AutopilotTriggerResponse `json:"triggers"`
|
|
}
|
|
|
|
// Owner (writer) sees the secret.
|
|
w = httptest.NewRecorder()
|
|
r = withURLParam(newRequest("GET", "/api/autopilots/"+apID+"?workspace_id="+testWorkspaceID, nil), "id", apID)
|
|
testHandler.GetAutopilot(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GetAutopilot as owner: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var ownerView getResp
|
|
if err := json.NewDecoder(w.Body).Decode(&ownerView); err != nil {
|
|
t.Fatalf("decode owner view: %v", err)
|
|
}
|
|
if len(ownerView.Triggers) != 1 {
|
|
t.Fatalf("owner view: expected 1 trigger, got %d", len(ownerView.Triggers))
|
|
}
|
|
if ownerView.Triggers[0].WebhookToken == nil || *ownerView.Triggers[0].WebhookToken == "" {
|
|
t.Fatalf("owner view: expected webhook_token to be present")
|
|
}
|
|
if ownerView.Triggers[0].WebhookPath == nil {
|
|
t.Fatalf("owner view: expected webhook_path to be present")
|
|
}
|
|
|
|
// Plain member (non-writer) sees the trigger but not the secret.
|
|
w = httptest.NewRecorder()
|
|
r = withURLParam(newRequestAs(stranger, "GET", "/api/autopilots/"+apID+"?workspace_id="+testWorkspaceID, nil), "id", apID)
|
|
testHandler.GetAutopilot(w, r)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("GetAutopilot as stranger: expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
var strangerView getResp
|
|
if err := json.NewDecoder(w.Body).Decode(&strangerView); err != nil {
|
|
t.Fatalf("decode stranger view: %v", err)
|
|
}
|
|
if len(strangerView.Triggers) != 1 {
|
|
t.Fatalf("stranger view: expected 1 trigger, got %d", len(strangerView.Triggers))
|
|
}
|
|
if strangerView.Triggers[0].Kind != "webhook" {
|
|
t.Fatalf("stranger view: expected webhook trigger to remain visible, got kind %q", strangerView.Triggers[0].Kind)
|
|
}
|
|
if strangerView.Triggers[0].WebhookToken != nil {
|
|
t.Fatalf("stranger view: webhook_token leaked: %v", *strangerView.Triggers[0].WebhookToken)
|
|
}
|
|
if strangerView.Triggers[0].WebhookPath != nil {
|
|
t.Fatalf("stranger view: webhook_path leaked: %v", *strangerView.Triggers[0].WebhookPath)
|
|
}
|
|
if strangerView.Triggers[0].WebhookURL != nil {
|
|
t.Fatalf("stranger view: webhook_url leaked: %v", *strangerView.Triggers[0].WebhookURL)
|
|
}
|
|
}
|