diff --git a/README.md b/README.md index 7537edb..7a74d5d 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,7 @@ Every request requires `Authorization: Bearer `. Responses are enve | -------- | ------------------------------ | -------------------- | ------------------------------------------------------------ | | `POST` | `/users` | `{username, kind}` | Create a user/service account (`kind`: `user` \| `service`); `409 user_exists` if the username is taken. | | `GET` | `/users/{id}` | — | Get an account. | +| `GET` | `/users/by-username/{username}` | — | Look up an account by username (name -> id resolution). | | `GET` | `/users/{id}/repositories` | — | List repositories owned by the account. | | `POST` | `/users/{id}/ssh-keys` | `{title, publicKey}` | Register an SSH public key. | | `GET` | `/users/{id}/ssh-keys` | — | List the account's SSH keys. | @@ -135,12 +136,14 @@ Every request requires `Authorization: Bearer `. Responses are enve | -------- | --------------------------------------------- | ----------------------------- | ----------------------------------------------------------------- | | `POST` | `/repositories` | `{ownerId, name, visibility}` | Create a repository (`visibility`: `public` \| `private`); `409 repository_exists` if the owner already has one with that name. | | `GET` | `/repositories/{id}` | — | Get repository metadata. | +| `GET` | `/repositories/by-path/{namespace}/{name}` | — | Look up a repository by owner username + name (name -> id resolution). | | `PUT` | `/repositories/{id}/visibility` | `{visibility}` | Change visibility (`public` \| `private`). | | `DELETE` | `/repositories/{id}` | — | Delete a repository (row + bare repo). | | `GET` | `/repositories/{id}/permissions` | — | List collaborators. | | `PUT` | `/repositories/{id}/permissions` | `{userId, role}` | Grant/update a collaborator role (`read` \| `write` \| `admin`). | | `DELETE` | `/repositories/{id}/permissions/{userId}` | — | Revoke a collaborator. | | `POST` | `/repositories/{id}/webhooks` | `{url}` | Register a push webhook; the signing secret is returned **once**. `409 webhook_exists` if the URL is already registered on the repo. | +| `GET` | `/repositories/{id}/webhooks` | — | List the repository's webhooks (never the secret). | | `DELETE` | `/repositories/{id}/webhooks/{hookId}` | — | Delete a webhook. | | `GET` | `/repositories/{id}/path-policies` | — | List the repository's path policies. | | `POST` | `/repositories/{id}/path-policies` | `{pattern, reason?}` | Block a path; see [Path policies](#path-policies). | diff --git a/internal/db/gen/users.sql.go b/internal/db/gen/users.sql.go index fc15c6d..7eb1e05 100644 --- a/internal/db/gen/users.sql.go +++ b/internal/db/gen/users.sql.go @@ -78,3 +78,22 @@ func (q *Queries) GetUser(ctx context.Context, id int64) (User, error) { ) return i, err } + +const getUserByUsername = `-- name: GetUserByUsername :one +select id, username, kind, is_admin, created_at_unix_ms, updated_at_unix_ms from users +where username=? limit 1 +` + +func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) { + row := q.db.QueryRowContext(ctx, getUserByUsername, username) + var i User + err := row.Scan( + &i.ID, + &i.Username, + &i.Kind, + &i.IsAdmin, + &i.CreatedAtUnixMs, + &i.UpdatedAtUnixMs, + ) + return i, err +} diff --git a/internal/db/queries/users.sql b/internal/db/queries/users.sql index c6959cb..89dcc2e 100644 --- a/internal/db/queries/users.sql +++ b/internal/db/queries/users.sql @@ -2,6 +2,10 @@ select * from users where id=? limit 1; +-- name: GetUserByUsername :one +select * from users +where username=? limit 1; + -- name: CreateUser :one insert into users ( username, kind diff --git a/internal/server/control/repositories/get.go b/internal/server/control/repositories/get.go index bbe6e71..f2c644f 100644 --- a/internal/server/control/repositories/get.go +++ b/internal/server/control/repositories/get.go @@ -28,3 +28,21 @@ func (h *handlers) getRepository(w http.ResponseWriter, r *http.Request) error { return response.Data(w, http.StatusOK, newRepository(repo)) } + +func (h *handlers) getRepositoryByPath(w http.ResponseWriter, r *http.Request) error { + namespace, name := chi.URLParam(r, "namespace"), chi.URLParam(r, "name") + if namespace == "" || name == "" { + return response.NewError(http.StatusBadRequest, response.CodeInvalidRequest, "namespace and name are required") + } + + repo, err := h.service.GetRepositoryByPath(r.Context(), namespace, name) + switch { + case errors.Is(err, reposervice.ErrRepositoryNotFound): + return response.NewError(http.StatusNotFound, response.CodeRepositoryNotFound, "repository not found") + case err != nil: + h.logger.Error("failed to get repository by path", zap.Error(err)) + return response.NewError(http.StatusInternalServerError, response.CodeInternalError, "failed to get repository") + } + + return response.Data(w, http.StatusOK, newRepository(repo)) +} diff --git a/internal/server/control/repositories/handlers.go b/internal/server/control/repositories/handlers.go index 218ca03..d0b5056 100644 --- a/internal/server/control/repositories/handlers.go +++ b/internal/server/control/repositories/handlers.go @@ -13,6 +13,7 @@ import ( type RepositoryManager interface { Create(ctx context.Context, ownerID int64, info domain.RepositoryInfo) (domain.Repository, error) Get(ctx context.Context, repositoryID int64) (domain.Repository, error) + GetRepositoryByPath(ctx context.Context, namespace, name string) (domain.Repository, error) Delete(ctx context.Context, repositoryID int64) error SetVisibility(ctx context.Context, repositoryID int64, visibility domain.RepoVisibility) (domain.Repository, error) ListByOwner(ctx context.Context, ownerID int64) ([]domain.Repository, error) @@ -43,6 +44,7 @@ func NewHandlers(logger *zap.Logger, service RepositoryManager) *handlers { func (h *handlers) RegisterRoutes(parent chi.Router) { parent.Route("/repositories", func(r chi.Router) { r.Post("/", response.Handler(h.logger, h.createRepository)) + r.Get("/by-path/{namespace}/{name}", response.Handler(h.logger, h.getRepositoryByPath)) r.Post("/{repositoryID}/blobs", response.Handler(h.logger, h.uploadBlob)) r.Post("/{repositoryID}/commits", response.Handler(h.logger, h.createCommit)) r.Get("/{repositoryID}", response.Handler(h.logger, h.getRepository)) diff --git a/internal/server/control/repositories/handlers_test.go b/internal/server/control/repositories/handlers_test.go index 36e4092..b44aecf 100644 --- a/internal/server/control/repositories/handlers_test.go +++ b/internal/server/control/repositories/handlers_test.go @@ -47,12 +47,19 @@ type fakeManager struct { createdRepo domain.Repository createErr error + + repoByPath domain.Repository + repoByPathErr error } func (f *fakeManager) Create(ctx context.Context, ownerID int64, info domain.RepositoryInfo) (domain.Repository, error) { return f.createdRepo, f.createErr } +func (f *fakeManager) GetRepositoryByPath(ctx context.Context, namespace, name string) (domain.Repository, error) { + return f.repoByPath, f.repoByPathErr +} + func (f *fakeManager) ListPathPolicies(ctx context.Context, repositoryID int64) ([]domain.PathPolicy, error) { return f.policyList, f.policyErr } @@ -577,6 +584,59 @@ func TestCreateRepository(t *testing.T) { } } +func TestGetRepositoryByPath(t *testing.T) { + cases := []struct { + name string + target string + svcErr error + wantStatus int + wantCode string + }{ + {"found", "/repositories/by-path/acme/api", nil, http.StatusOK, ""}, + {"numeric namespace and name", "/repositories/by-path/123/456", nil, http.StatusOK, ""}, + {"not found", "/repositories/by-path/acme/nope", reposervice.ErrRepositoryNotFound, http.StatusNotFound, "repository_not_found"}, + {"internal", "/repositories/by-path/acme/api", io.ErrUnexpectedEOF, http.StatusInternalServerError, "internal_error"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + fake := &fakeManager{ + repoByPath: domain.Repository{ID: 7, OwnerID: 3, RepositoryName: "api", Visibility: domain.RepoVisibilityPrivate}, + repoByPathErr: tc.svcErr, + } + rec := httptest.NewRecorder() + newTestRouter(fake).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, tc.target, nil)) + + if rec.Code != tc.wantStatus { + t.Fatalf("status = %d, want %d: %s", rec.Code, tc.wantStatus, rec.Body.String()) + } + if tc.wantCode == "" { + var body struct { + Data Repository `json:"data"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatal(err) + } + if body.Data.ID != 7 || body.Data.Name != "api" { + t.Errorf("body = %+v", body.Data) + } + return + } + var body struct { + Error struct { + Code string `json:"code"` + } `json:"error"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatal(err) + } + if body.Error.Code != tc.wantCode { + t.Errorf("code = %q, want %q", body.Error.Code, tc.wantCode) + } + }) + } +} + func TestPathPolicies(t *testing.T) { policy := domain.PathPolicy{ID: 3, RepositoryID: 7, Pattern: "runtime", Kind: domain.PathPolicyBlock, Reason: "deploy state"} diff --git a/internal/server/control/users/getuser.go b/internal/server/control/users/getuser.go index b72a43f..9d389eb 100644 --- a/internal/server/control/users/getuser.go +++ b/internal/server/control/users/getuser.go @@ -28,3 +28,21 @@ func (h *handlers) getUser(w http.ResponseWriter, r *http.Request) error { return response.Data(w, http.StatusOK, newUserResponse(account)) } + +func (h *handlers) getUserByUsername(w http.ResponseWriter, r *http.Request) error { + username := chi.URLParam(r, "username") + if username == "" { + return response.NewError(http.StatusBadRequest, response.CodeInvalidRequest, "username is required") + } + + account, err := h.users.GetByUsername(r.Context(), username) + switch { + case errors.Is(err, usersservice.ErrUserNotFound): + return response.NewError(http.StatusNotFound, response.CodeUserNotFound, "user not found") + case err != nil: + h.logger.Error("failed to get user by username", zap.Error(err)) + return response.NewError(http.StatusInternalServerError, response.CodeInternalError, "failed to get user") + } + + return response.Data(w, http.StatusOK, newUserResponse(account)) +} diff --git a/internal/server/control/users/handlers.go b/internal/server/control/users/handlers.go index fc0ad67..3b2f24a 100644 --- a/internal/server/control/users/handlers.go +++ b/internal/server/control/users/handlers.go @@ -13,6 +13,7 @@ import ( type UserManager interface { Create(ctx context.Context, info domain.UserInfo) (domain.Account, error) Get(ctx context.Context, userID int64) (domain.Account, error) + GetByUsername(ctx context.Context, username string) (domain.Account, error) } type CredentialManager interface { @@ -43,6 +44,7 @@ func NewHandlers(logger *zap.Logger, users UserManager, creds CredentialManager) func (h *handlers) RegisterRoutes(parent chi.Router) { parent.Route("/users", func(r chi.Router) { r.Post("/", response.Handler(h.logger, h.createUser)) + r.Get("/by-username/{username}", response.Handler(h.logger, h.getUserByUsername)) r.Get("/{userID}", response.Handler(h.logger, h.getUser)) r.Get("/{userID}/ssh-keys", response.Handler(h.logger, h.listSSHKeys)) diff --git a/internal/server/control/users/handlers_test.go b/internal/server/control/users/handlers_test.go index 9ed47cf..98d2f18 100644 --- a/internal/server/control/users/handlers_test.go +++ b/internal/server/control/users/handlers_test.go @@ -26,6 +26,10 @@ func (f fakeUserManager) Create(ctx context.Context, info domain.UserInfo) (doma return f.account, f.err } +func (f fakeUserManager) GetByUsername(ctx context.Context, username string) (domain.Account, error) { + return f.account, f.err +} + // newTestRouter mounts the handlers the same way the control server does func newTestRouter(users UserManager) http.Handler { r := chi.NewRouter() @@ -33,6 +37,60 @@ func newTestRouter(users UserManager) http.Handler { return r } +func TestGetUserByUsername(t *testing.T) { + cases := []struct { + name string + target string + svcErr error + wantStatus int + wantCode string + }{ + {"found", "/users/by-username/alice", nil, http.StatusOK, ""}, + {"numeric username", "/users/by-username/12345", nil, http.StatusOK, ""}, + {"not found", "/users/by-username/ghost", usersservice.ErrUserNotFound, http.StatusNotFound, "user_not_found"}, + {"internal", "/users/by-username/alice", context.DeadlineExceeded, http.StatusInternalServerError, "internal_error"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + router := newTestRouter(fakeUserManager{ + account: domain.Account{UserID: 7, Username: "alice", Kind: domain.UserKindUser}, + err: tc.svcErr, + }) + + rec := httptest.NewRecorder() + router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, tc.target, nil)) + + if rec.Code != tc.wantStatus { + t.Fatalf("status = %d, want %d (body: %s)", rec.Code, tc.wantStatus, rec.Body.String()) + } + if tc.wantCode == "" { + var envelope struct { + Data UserResponse `json:"data"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil { + t.Fatalf("decode data envelope: %v", err) + } + if envelope.Data.ID != 7 || envelope.Data.Username != "alice" { + t.Errorf("body = %+v", envelope.Data) + } + return + } + var envelope struct { + Error struct { + Code string `json:"code"` + } `json:"error"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &envelope); err != nil { + t.Fatalf("decode error envelope: %v", err) + } + if envelope.Error.Code != tc.wantCode { + t.Errorf("code = %q, want %q", envelope.Error.Code, tc.wantCode) + } + }) + } +} + func TestCreateUser(t *testing.T) { tests := []struct { name string diff --git a/internal/server/control/webhooks/handlers.go b/internal/server/control/webhooks/handlers.go index a8de2fe..d8a0e4e 100644 --- a/internal/server/control/webhooks/handlers.go +++ b/internal/server/control/webhooks/handlers.go @@ -12,6 +12,7 @@ import ( type WebhookManager interface { RegisterWebhook(ctx context.Context, repoID int64, url string) (domain.Webhook, error) DeleteWebhook(ctx context.Context, webhookID, repositoryID int64) error + ListWebhooks(ctx context.Context, repositoryID int64) ([]domain.Webhook, error) } type handlers struct { @@ -29,6 +30,7 @@ func NewHandlers(logger *zap.Logger, webhooks WebhookManager) *handlers { func (h *handlers) RegisterRoutes(parent chi.Router) { parent.Route("/repositories/{repositoryID}/webhooks", func(r chi.Router) { r.Post("/", response.Handler(h.logger, h.createWebhook)) + r.Get("/", response.Handler(h.logger, h.listWebhooks)) r.Delete("/{webhookID}", response.Handler(h.logger, h.deleteWebhook)) }) } diff --git a/internal/server/control/webhooks/handlers_test.go b/internal/server/control/webhooks/handlers_test.go index 4557bcc..c2b9be1 100644 --- a/internal/server/control/webhooks/handlers_test.go +++ b/internal/server/control/webhooks/handlers_test.go @@ -29,6 +29,13 @@ func (f fakeManager) DeleteWebhook(ctx context.Context, webhookID, repositoryID return f.err } +func (f fakeManager) ListWebhooks(ctx context.Context, repositoryID int64) ([]domain.Webhook, error) { + if f.err != nil { + return nil, f.err + } + return []domain.Webhook{f.webhook}, nil +} + // newTestRouter mounts the handlers the same way the control server does func newTestRouter(webhooks WebhookManager) http.Handler { r := chi.NewRouter() @@ -36,6 +43,46 @@ func newTestRouter(webhooks WebhookManager) http.Handler { return r } +func TestListWebhooks(t *testing.T) { + t.Run("ok, never leaks the secret", func(t *testing.T) { + fake := fakeManager{webhook: domain.Webhook{ID: 3, RepositoryID: 7, URL: "https://example.com/hook", Secret: "super-secret"}} + rec := httptest.NewRecorder() + newTestRouter(fake).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/repositories/7/webhooks", nil)) + + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, body %s", rec.Code, rec.Body.String()) + } + var body struct { + Data []WebhookListItem `json:"data"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { + t.Fatal(err) + } + if len(body.Data) != 1 || body.Data[0].ID != 3 || body.Data[0].URL != "https://example.com/hook" { + t.Errorf("body = %+v", body.Data) + } + if strings.Contains(rec.Body.String(), "secret") || strings.Contains(rec.Body.String(), "super-secret") { + t.Errorf("list response leaks the secret: %s", rec.Body.String()) + } + }) + + t.Run("invalid repo id", func(t *testing.T) { + rec := httptest.NewRecorder() + newTestRouter(fakeManager{}).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/repositories/abc/webhooks", nil)) + if rec.Code != http.StatusBadRequest { + t.Fatalf("status = %d, body %s", rec.Code, rec.Body.String()) + } + }) + + t.Run("internal", func(t *testing.T) { + rec := httptest.NewRecorder() + newTestRouter(fakeManager{err: io.ErrUnexpectedEOF}).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/repositories/7/webhooks", nil)) + if rec.Code != http.StatusInternalServerError { + t.Fatalf("status = %d, body %s", rec.Code, rec.Body.String()) + } + }) +} + func TestCreateWebhook(t *testing.T) { cases := []struct { name string diff --git a/internal/server/control/webhooks/list.go b/internal/server/control/webhooks/list.go new file mode 100644 index 0000000..d61ae38 --- /dev/null +++ b/internal/server/control/webhooks/list.go @@ -0,0 +1,25 @@ +package webhooks + +import ( + "net/http" + "strconv" + + "github.com/Axenos-dev/HeadlessGit/internal/server/response" + "github.com/go-chi/chi/v5" + "go.uber.org/zap" +) + +func (h *handlers) listWebhooks(w http.ResponseWriter, r *http.Request) error { + repoID, err := strconv.ParseInt(chi.URLParam(r, "repositoryID"), 10, 64) + if err != nil { + return response.NewError(http.StatusBadRequest, response.CodeInvalidRequest, "invalid repository id") + } + + webhooks, err := h.webhooks.ListWebhooks(r.Context(), repoID) + if err != nil { + h.logger.Error("failed to list webhooks", zap.Error(err)) + return response.NewError(http.StatusInternalServerError, response.CodeInternalError, "failed to list webhooks") + } + + return response.Data(w, http.StatusOK, newWebhookListItems(webhooks)) +} diff --git a/internal/server/control/webhooks/types.go b/internal/server/control/webhooks/types.go index 2eee8c6..1d8e92b 100644 --- a/internal/server/control/webhooks/types.go +++ b/internal/server/control/webhooks/types.go @@ -38,3 +38,17 @@ func newWebhookResponse(w domain.Webhook) WebhookResponse { CreatedAt: w.CreatedAt, } } + +type WebhookListItem struct { + ID int64 `json:"id"` + URL string `json:"url"` + CreatedAt time.Time `json:"createdAt"` +} + +func newWebhookListItems(webhooks []domain.Webhook) []WebhookListItem { + out := make([]WebhookListItem, len(webhooks)) + for i, w := range webhooks { + out[i] = WebhookListItem{ID: w.ID, URL: w.URL, CreatedAt: w.CreatedAt} + } + return out +} diff --git a/internal/services/users/registry.go b/internal/services/users/registry.go index 0739321..d8462e0 100644 --- a/internal/services/users/registry.go +++ b/internal/services/users/registry.go @@ -21,6 +21,10 @@ func (r *UserRegistry) GetUser(ctx context.Context, userID int64) (gen.User, err return r.db.GetUser(ctx, userID) } +func (r *UserRegistry) GetUserByUsername(ctx context.Context, username string) (gen.User, error) { + return r.db.GetUserByUsername(ctx, username) +} + func (r *UserRegistry) CreateUser(ctx context.Context, username, kind string) (gen.User, error) { return r.db.CreateUser(ctx, gen.CreateUserParams{ Username: username, diff --git a/internal/services/users/service.go b/internal/services/users/service.go index e8199f9..d08126a 100644 --- a/internal/services/users/service.go +++ b/internal/services/users/service.go @@ -12,6 +12,7 @@ import ( type Registry interface { GetUser(ctx context.Context, userID int64) (gen.User, error) + GetUserByUsername(ctx context.Context, username string) (gen.User, error) CreateUser(ctx context.Context, username, kind string) (gen.User, error) } @@ -48,6 +49,17 @@ func (s *Service) Get(ctx context.Context, userID int64) (domain.Account, error) return toAccount(user), nil } +func (s *Service) GetByUsername(ctx context.Context, username string) (domain.Account, error) { + user, err := s.registry.GetUserByUsername(ctx, username) + if errors.Is(err, sql.ErrNoRows) { + return domain.Account{}, ErrUserNotFound + } + if err != nil { + return domain.Account{}, err + } + return toAccount(user), nil +} + func toAccount(u gen.User) domain.Account { return domain.Account{ UserID: u.ID, diff --git a/internal/services/users/service_test.go b/internal/services/users/service_test.go index de0eb44..bc7cad8 100644 --- a/internal/services/users/service_test.go +++ b/internal/services/users/service_test.go @@ -19,6 +19,10 @@ func (f fakeRegistry) GetUser(ctx context.Context, userID int64) (gen.User, erro return f.user, f.err } +func (f fakeRegistry) GetUserByUsername(ctx context.Context, username string) (gen.User, error) { + return f.user, f.err +} + func (f fakeRegistry) CreateUser(ctx context.Context, username, kind string) (gen.User, error) { return f.user, f.err } @@ -51,3 +55,23 @@ func TestCreate(t *testing.T) { } }) } + +func TestGetByUsername(t *testing.T) { + t.Run("ok", func(t *testing.T) { + svc := NewService(fakeRegistry{user: gen.User{ID: 7, Username: "alice", Kind: "user"}}) + account, err := svc.GetByUsername(context.Background(), "alice") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if account.UserID != 7 || account.Username != "alice" { + t.Errorf("unexpected account: %+v", account) + } + }) + + t.Run("not found", func(t *testing.T) { + svc := NewService(fakeRegistry{err: sql.ErrNoRows}) + if _, err := svc.GetByUsername(context.Background(), "ghost"); !errors.Is(err, ErrUserNotFound) { + t.Errorf("want ErrUserNotFound, got %v", err) + } + }) +} diff --git a/internal/services/webhooks/service.go b/internal/services/webhooks/service.go index 698f19b..d07e456 100644 --- a/internal/services/webhooks/service.go +++ b/internal/services/webhooks/service.go @@ -66,6 +66,18 @@ func (s *Service) DeleteWebhook(ctx context.Context, webhookID, repositoryID int return s.registry.DeleteWebhook(ctx, webhookID, repositoryID) } +func (s *Service) ListWebhooks(ctx context.Context, repositoryID int64) ([]domain.Webhook, error) { + rows, err := s.registry.ListWebhooksForRepository(ctx, repositoryID) + if err != nil { + return nil, err + } + out := make([]domain.Webhook, len(rows)) + for i, row := range rows { + out[i] = toDomain(row) + } + return out, nil +} + func (s *Service) DispatchEvent(ctx context.Context, event domain.RepositoryEvent) error { select { case s.eventsCh <- event: diff --git a/internal/services/webhooks/service_test.go b/internal/services/webhooks/service_test.go index 3a037f8..3ab283b 100644 --- a/internal/services/webhooks/service_test.go +++ b/internal/services/webhooks/service_test.go @@ -20,6 +20,13 @@ func (f fakeRegistry) CreateWebhook(ctx context.Context, repoID int64, secret, u return f.webhook, f.err } +func (f fakeRegistry) ListWebhooksForRepository(ctx context.Context, repoID int64) ([]gen.Webhook, error) { + if f.err != nil { + return nil, f.err + } + return []gen.Webhook{f.webhook}, nil +} + func TestRegisterWebhook(t *testing.T) { t.Run("ok", func(t *testing.T) { svc := NewService(zap.NewNop(), fakeRegistry{webhook: gen.Webhook{ID: 3, RepositoryID: 7, Url: "https://example.com/hook", Secret: "s"}}) @@ -48,3 +55,14 @@ func TestRegisterWebhook(t *testing.T) { } }) } + +func TestListWebhooks(t *testing.T) { + svc := NewService(zap.NewNop(), fakeRegistry{webhook: gen.Webhook{ID: 3, RepositoryID: 7, Url: "https://example.com/hook", Secret: "s"}}) + webhooks, err := svc.ListWebhooks(context.Background(), 7) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(webhooks) != 1 || webhooks[0].ID != 3 || webhooks[0].URL != "https://example.com/hook" { + t.Errorf("unexpected webhooks: %+v", webhooks) + } +}