310 lines
8.9 KiB
Go
310 lines
8.9 KiB
Go
package sshkey
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"proxui/backend/internal/auth"
|
|
"proxui/backend/internal/membership"
|
|
"proxui/backend/internal/rbac"
|
|
)
|
|
|
|
func TestListTenantKeysReturnsKeys(t *testing.T) {
|
|
repository := &stubRepository{
|
|
listFound: true,
|
|
keys: []SSHKey{{
|
|
ID: "key-1",
|
|
TenantID: "tenant-1",
|
|
Name: "laptop",
|
|
PublicKey: "ssh-ed25519 AAAAC3...",
|
|
CreatedAt: time.Date(2026, 6, 11, 12, 0, 0, 0, time.UTC),
|
|
}},
|
|
}
|
|
handler := NewHandler(repository)
|
|
req := requestWithPrincipal(http.MethodGet, "/tenants/tenant-1/ssh-keys")
|
|
req.SetPathValue("tenantID", "tenant-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ListTenantKeys(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if repository.profileID != "profile-1" {
|
|
t.Fatalf("profileID = %q, want profile-1", repository.profileID)
|
|
}
|
|
if repository.tenantID != "tenant-1" {
|
|
t.Fatalf("tenantID = %q, want tenant-1", repository.tenantID)
|
|
}
|
|
|
|
var response struct {
|
|
Data []SSHKey `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("unmarshal response: %v", err)
|
|
}
|
|
if len(response.Data) != 1 {
|
|
t.Fatalf("len(data) = %d, want 1", len(response.Data))
|
|
}
|
|
if response.Data[0].ID != "key-1" {
|
|
t.Fatalf("key ID = %q, want key-1", response.Data[0].ID)
|
|
}
|
|
}
|
|
|
|
func TestListTenantKeysReturnsEmptyListForTenantWithNoKeys(t *testing.T) {
|
|
handler := NewHandler(&stubRepository{listFound: true, keys: []SSHKey{}})
|
|
req := requestWithPrincipal(http.MethodGet, "/tenants/tenant-1/ssh-keys")
|
|
req.SetPathValue("tenantID", "tenant-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ListTenantKeys(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
var response struct {
|
|
Data []SSHKey `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("unmarshal response: %v", err)
|
|
}
|
|
if response.Data == nil {
|
|
t.Fatal("data should be an empty slice, not nil")
|
|
}
|
|
}
|
|
|
|
func TestListTenantKeysReturnsNotFound(t *testing.T) {
|
|
handler := NewHandler(&stubRepository{listFound: false})
|
|
req := requestWithPrincipal(http.MethodGet, "/tenants/tenant-1/ssh-keys")
|
|
req.SetPathValue("tenantID", "tenant-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ListTenantKeys(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestGetKeyReturnsKey(t *testing.T) {
|
|
repository := &stubRepository{
|
|
getFound: true,
|
|
key: SSHKey{
|
|
ID: "key-1",
|
|
TenantID: "tenant-1",
|
|
Name: "laptop",
|
|
PublicKey: "ssh-ed25519 AAAAC3...",
|
|
},
|
|
}
|
|
handler := NewHandler(repository)
|
|
req := requestWithPrincipalAndMembership(http.MethodGet, "/ssh-keys/key-1", "tenant-1", "owner")
|
|
req.SetPathValue("keyID", "key-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.GetKey(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if repository.keyID != "key-1" {
|
|
t.Fatalf("keyID = %q, want key-1", repository.keyID)
|
|
}
|
|
var response SSHKey
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("unmarshal response: %v", err)
|
|
}
|
|
if response.ID != "key-1" {
|
|
t.Fatalf("key ID = %q, want key-1", response.ID)
|
|
}
|
|
}
|
|
|
|
func TestGetKeyReturnsNotFound(t *testing.T) {
|
|
handler := NewHandler(&stubRepository{getFound: false})
|
|
req := requestWithPrincipalAndMembership(http.MethodGet, "/ssh-keys/key-1", "tenant-1", "owner")
|
|
req.SetPathValue("keyID", "key-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.GetKey(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestCreateKeyReturnsCreated(t *testing.T) {
|
|
repository := &stubRepository{
|
|
createFound: true,
|
|
createResult: SSHKey{
|
|
ID: "key-1",
|
|
TenantID: "tenant-1",
|
|
Name: "laptop",
|
|
PublicKey: "ssh-ed25519 AAAAC3...",
|
|
},
|
|
}
|
|
handler := NewHandler(repository)
|
|
body := `{"name":"laptop","public_key":"ssh-ed25519 AAAAC3..."}`
|
|
req := requestWithPrincipalAndMembership(http.MethodPost, "/tenants/tenant-1/ssh-keys", "tenant-1", "owner")
|
|
req.SetPathValue("tenantID", "tenant-1")
|
|
req.Body = io.NopCloser(strings.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.CreateKey(rec, req)
|
|
|
|
if rec.Code != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusCreated)
|
|
}
|
|
var response SSHKey
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
|
|
t.Fatalf("unmarshal response: %v", err)
|
|
}
|
|
if response.ID != "key-1" {
|
|
t.Fatalf("key ID = %q, want key-1", response.ID)
|
|
}
|
|
}
|
|
|
|
func TestCreateKeyReturnsForbiddenForViewer(t *testing.T) {
|
|
handler := NewHandler(&stubRepository{})
|
|
body := `{"name":"laptop","public_key":"ssh-ed25519 AAAAC3..."}`
|
|
req := requestWithPrincipalAndMembership(http.MethodPost, "/tenants/tenant-1/ssh-keys", "tenant-1", "viewer")
|
|
req.SetPathValue("tenantID", "tenant-1")
|
|
req.Body = io.NopCloser(strings.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.CreateKey(rec, req)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
|
|
}
|
|
}
|
|
|
|
func TestDeleteKeyReturnsNoContent(t *testing.T) {
|
|
repository := &stubRepository{
|
|
deleteResult: true,
|
|
deleteMember: true,
|
|
}
|
|
handler := NewHandler(repository)
|
|
req := requestWithPrincipalAndMembership(http.MethodDelete, "/ssh-keys/key-1", "tenant-1", "owner")
|
|
req.SetPathValue("keyID", "key-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.DeleteKey(rec, req)
|
|
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNoContent)
|
|
}
|
|
if repository.deleteKeyID != "key-1" {
|
|
t.Fatalf("keyID = %q, want key-1", repository.deleteKeyID)
|
|
}
|
|
}
|
|
|
|
func TestDeleteKeyReturnsNotFound(t *testing.T) {
|
|
repo := &stubRepository{deleteResult: false, deleteMember: true}
|
|
handler := NewHandler(repo)
|
|
req := requestWithPrincipalAndMembership(http.MethodDelete, "/ssh-keys/key-1", "tenant-1", "owner")
|
|
req.SetPathValue("keyID", "key-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.DeleteKey(rec, req)
|
|
|
|
if rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusNotFound)
|
|
}
|
|
}
|
|
|
|
func TestCreateKeyRejectsEmptyName(t *testing.T) {
|
|
handler := NewHandler(&stubRepository{})
|
|
body := `{"name":" ","public_key":"ssh-ed25519 AAAAC3..."}`
|
|
req := requestWithPrincipalAndMembership(http.MethodPost, "/tenants/tenant-1/ssh-keys", "tenant-1", "owner")
|
|
req.SetPathValue("tenantID", "tenant-1")
|
|
req.Body = io.NopCloser(strings.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.CreateKey(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func TestListTenantKeysServerError(t *testing.T) {
|
|
handler := NewHandler(&stubRepository{err: errors.New("db failed")})
|
|
req := requestWithPrincipal(http.MethodGet, "/tenants/tenant-1/ssh-keys")
|
|
req.SetPathValue("tenantID", "tenant-1")
|
|
rec := httptest.NewRecorder()
|
|
|
|
handler.ListTenantKeys(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
|
|
func requestWithPrincipal(method string, target string) *http.Request {
|
|
req := httptest.NewRequest(method, target, nil)
|
|
return req.WithContext(auth.ContextWithPrincipal(req.Context(), auth.Principal{
|
|
Subject: "profile-1",
|
|
Email: "user@example.test",
|
|
Role: "authenticated",
|
|
}))
|
|
}
|
|
|
|
func requestWithPrincipalAndMembership(method string, target string, tenantID string, role string) *http.Request {
|
|
req := httptest.NewRequest(method, target, nil)
|
|
ctx := auth.ContextWithPrincipal(req.Context(), auth.Principal{
|
|
Subject: "profile-1",
|
|
Email: "user@example.test",
|
|
Role: "authenticated",
|
|
})
|
|
ctx = membership.ContextWithMembership(ctx, membership.Membership{
|
|
TenantID: tenantID,
|
|
Role: rbac.Role(role),
|
|
})
|
|
return req.WithContext(ctx)
|
|
}
|
|
|
|
type stubRepository struct {
|
|
profileID string
|
|
tenantID string
|
|
keyID string
|
|
deleteKeyID string
|
|
keys []SSHKey
|
|
key SSHKey
|
|
createResult SSHKey
|
|
listFound bool
|
|
getFound bool
|
|
createFound bool
|
|
deleteResult bool
|
|
deleteMember bool
|
|
err error
|
|
}
|
|
|
|
func (s *stubRepository) List(_ context.Context, profileID string, tenantID string) ([]SSHKey, bool, error) {
|
|
s.profileID = profileID
|
|
s.tenantID = tenantID
|
|
return s.keys, s.listFound, s.err
|
|
}
|
|
|
|
func (s *stubRepository) Get(_ context.Context, profileID string, keyID string) (SSHKey, bool, error) {
|
|
s.profileID = profileID
|
|
s.keyID = keyID
|
|
return s.key, s.getFound, s.err
|
|
}
|
|
|
|
func (s *stubRepository) Create(_ context.Context, profileID string, tenantID string, name string, publicKey string) (SSHKey, bool, error) {
|
|
s.profileID = profileID
|
|
s.tenantID = tenantID
|
|
return s.createResult, s.createFound, s.err
|
|
}
|
|
|
|
func (s *stubRepository) Delete(_ context.Context, profileID string, keyID string) (bool, bool, error) {
|
|
s.profileID = profileID
|
|
s.deleteKeyID = keyID
|
|
return s.deleteResult, s.deleteMember, s.err
|
|
} |