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 }