clients: expand Go, Python, Rust client APIs
- Add TOTP enrollment/confirmation/removal to all clients - Add password change and admin set-password endpoints - Add account listing, status update, and tag management - Add audit log listing with filter support - Add policy rule CRUD operations - Expand test coverage for all new endpoints across clients - Fix .gitignore to exclude built binaries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// All tests use inline httptest.NewServer mocks to keep this module
|
||||
// self-contained (no cross-module imports).
|
||||
package mciasgoclient_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -9,12 +10,14 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mciasgoclient "git.wntrmute.dev/kyle/mcias/clients/go"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
// newTestClient creates a client pointed at the given test server URL.
|
||||
|
||||
func newTestClient(t *testing.T, serverURL string) *mciasgoclient.Client {
|
||||
t.Helper()
|
||||
c, err := mciasgoclient.New(serverURL, mciasgoclient.Options{})
|
||||
@@ -23,19 +26,21 @@ func newTestClient(t *testing.T, serverURL string) *mciasgoclient.Client {
|
||||
}
|
||||
return c
|
||||
}
|
||||
// writeJSON is a shorthand for writing a JSON response.
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
// writeError writes a JSON error body with the given status code.
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, msg string) {
|
||||
writeJSON(w, status, map[string]string{"error": msg})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestNew
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
c, err := mciasgoclient.New("https://example.com", mciasgoclient.Options{})
|
||||
if err != nil {
|
||||
@@ -45,6 +50,7 @@ func TestNew(t *testing.T) {
|
||||
t.Fatal("expected non-nil client")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWithPresetToken(t *testing.T) {
|
||||
c, err := mciasgoclient.New("https://example.com", mciasgoclient.Options{Token: "preset-tok"})
|
||||
if err != nil {
|
||||
@@ -54,15 +60,18 @@ func TestNewWithPresetToken(t *testing.T) {
|
||||
t.Errorf("expected preset-tok, got %q", c.Token())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewBadCACert(t *testing.T) {
|
||||
_, err := mciasgoclient.New("https://example.com", mciasgoclient.Options{CACertPath: "/nonexistent/ca.pem"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing CA cert file")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestHealth
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestHealth(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/health" || r.Method != http.MethodGet {
|
||||
@@ -77,9 +86,7 @@ func TestHealth(t *testing.T) {
|
||||
t.Fatalf("Health: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestHealthError
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestHealthError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusServiceUnavailable, "service unavailable")
|
||||
@@ -98,9 +105,11 @@ func TestHealthError(t *testing.T) {
|
||||
t.Errorf("expected StatusCode 503, got %d", srvErr.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestGetPublicKey
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetPublicKey(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/keys/public" {
|
||||
@@ -131,9 +140,11 @@ func TestGetPublicKey(t *testing.T) {
|
||||
t.Error("expected non-empty x")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestLogin
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/auth/login" || r.Method != http.MethodPost {
|
||||
@@ -157,14 +168,11 @@ func TestLogin(t *testing.T) {
|
||||
if exp == "" {
|
||||
t.Error("expected non-empty expires_at")
|
||||
}
|
||||
// Token must be stored in the client.
|
||||
if c.Token() != "tok-abc123" {
|
||||
t.Errorf("Token() = %q, want tok-abc123", c.Token())
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestLoginUnauthorized
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestLoginUnauthorized(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusUnauthorized, "invalid credentials")
|
||||
@@ -180,16 +188,17 @@ func TestLoginUnauthorized(t *testing.T) {
|
||||
t.Errorf("expected MciasAuthError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestLogout
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/auth/login":
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"token": "tok-logout",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"token": "tok-logout", "expires_at": "2099-01-01T00:00:00Z",
|
||||
})
|
||||
case "/v1/auth/logout":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -212,21 +221,21 @@ func TestLogout(t *testing.T) {
|
||||
t.Errorf("expected empty token after logout, got %q", c.Token())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestRenewToken
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRenewToken(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/v1/auth/login":
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"token": "tok-old",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"token": "tok-old", "expires_at": "2099-01-01T00:00:00Z",
|
||||
})
|
||||
case "/v1/auth/renew":
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"token": "tok-new",
|
||||
"expires_at": "2099-06-01T00:00:00Z",
|
||||
"token": "tok-new", "expires_at": "2099-06-01T00:00:00Z",
|
||||
})
|
||||
default:
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
@@ -248,9 +257,125 @@ func TestRenewToken(t *testing.T) {
|
||||
t.Errorf("Token() = %q, want tok-new", c.Token())
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestEnrollTOTP / TestConfirmTOTP
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestEnrollTOTP(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/auth/totp/enroll" || r.Method != http.MethodPost {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"secret": "JBSWY3DPEHPK3PXP",
|
||||
"otpauth_uri": "otpauth://totp/MCIAS:alice?secret=JBSWY3DPEHPK3PXP&issuer=MCIAS",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
resp, err := c.EnrollTOTP()
|
||||
if err != nil {
|
||||
t.Fatalf("EnrollTOTP: %v", err)
|
||||
}
|
||||
if resp.Secret != "JBSWY3DPEHPK3PXP" {
|
||||
t.Errorf("expected secret=JBSWY3DPEHPK3PXP, got %q", resp.Secret)
|
||||
}
|
||||
if resp.OTPAuthURI == "" {
|
||||
t.Error("expected non-empty otpauth_uri")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmTOTP(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/auth/totp/confirm" || r.Method != http.MethodPost {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
if err := c.ConfirmTOTP("123456"); err != nil {
|
||||
t.Fatalf("ConfirmTOTP: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmTOTPBadCode(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "invalid TOTP code")
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
err := c.ConfirmTOTP("000000")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for bad TOTP code")
|
||||
}
|
||||
var inputErr *mciasgoclient.MciasInputError
|
||||
if !errors.As(err, &inputErr) {
|
||||
t.Errorf("expected MciasInputError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestChangePassword
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestChangePassword(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/auth/password" || r.Method != http.MethodPut {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
if err := c.ChangePassword("old-s3cr3t", "new-s3cr3t-long"); err != nil {
|
||||
t.Fatalf("ChangePassword: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangePasswordWrongCurrent(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusUnauthorized, "current password is incorrect")
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
err := c.ChangePassword("wrong", "new-s3cr3t-long")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong current password")
|
||||
}
|
||||
var authErr *mciasgoclient.MciasAuthError
|
||||
if !errors.As(err, &authErr) {
|
||||
t.Errorf("expected MciasAuthError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestRemoveTOTP
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRemoveTOTP(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/auth/totp" || r.Method != http.MethodDelete {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
if err := c.RemoveTOTP("acct-uuid-42"); err != nil {
|
||||
t.Fatalf("RemoveTOTP: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestValidateToken
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidateToken(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/token/validate" {
|
||||
@@ -258,10 +383,8 @@ func TestValidateToken(t *testing.T) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"valid": true,
|
||||
"sub": "user-uuid-1",
|
||||
"roles": []string{"admin"},
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"valid": true, "sub": "user-uuid-1",
|
||||
"roles": []string{"admin"}, "expires_at": "2099-01-01T00:00:00Z",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -277,15 +400,10 @@ func TestValidateToken(t *testing.T) {
|
||||
t.Errorf("expected sub=user-uuid-1, got %q", claims.Sub)
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestValidateTokenInvalid
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestValidateTokenInvalid(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Server returns 200 with valid=false for an expired/revoked token.
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"valid": false,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"valid": false})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
@@ -297,9 +415,11 @@ func TestValidateTokenInvalid(t *testing.T) {
|
||||
t.Error("expected claims.Valid = false")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestCreateAccount
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreateAccount(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/accounts" || r.Method != http.MethodPost {
|
||||
@@ -307,13 +427,9 @@ func TestCreateAccount(t *testing.T) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": "acct-uuid-1",
|
||||
"username": "bob",
|
||||
"account_type": "human",
|
||||
"status": "active",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
"totp_enabled": false,
|
||||
"id": "acct-uuid-1", "username": "bob", "account_type": "human",
|
||||
"status": "active", "created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z", "totp_enabled": false,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -329,9 +445,7 @@ func TestCreateAccount(t *testing.T) {
|
||||
t.Errorf("expected username=bob, got %q", acct.Username)
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestCreateAccountConflict
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreateAccountConflict(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusConflict, "username already exists")
|
||||
@@ -347,21 +461,19 @@ func TestCreateAccountConflict(t *testing.T) {
|
||||
t.Errorf("expected MciasConflictError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestListAccounts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestListAccounts(t *testing.T) {
|
||||
accounts := []map[string]interface{}{
|
||||
{
|
||||
"id": "acct-1", "username": "alice", "account_type": "human",
|
||||
{"id": "acct-1", "username": "alice", "account_type": "human",
|
||||
"status": "active", "created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z", "totp_enabled": false,
|
||||
},
|
||||
{
|
||||
"id": "acct-2", "username": "bob", "account_type": "human",
|
||||
"updated_at": "2024-01-01T00:00:00Z", "totp_enabled": false},
|
||||
{"id": "acct-2", "username": "bob", "account_type": "human",
|
||||
"status": "active", "created_at": "2024-01-02T00:00:00Z",
|
||||
"updated_at": "2024-01-02T00:00:00Z", "totp_enabled": false,
|
||||
},
|
||||
"updated_at": "2024-01-02T00:00:00Z", "totp_enabled": false},
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/accounts" || r.Method != http.MethodGet {
|
||||
@@ -383,27 +495,21 @@ func TestListAccounts(t *testing.T) {
|
||||
t.Errorf("expected alice, got %q", list[0].Username)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestGetAccount
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetAccount(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(r.URL.Path, "/v1/accounts/") {
|
||||
if r.Method != http.MethodGet || !strings.HasPrefix(r.URL.Path, "/v1/accounts/") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"id": "acct-uuid-42",
|
||||
"username": "carol",
|
||||
"account_type": "human",
|
||||
"status": "active",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
"totp_enabled": false,
|
||||
"id": "acct-uuid-42", "username": "carol", "account_type": "human",
|
||||
"status": "active", "created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z", "totp_enabled": false,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -416,38 +522,30 @@ func TestGetAccount(t *testing.T) {
|
||||
t.Errorf("expected acct-uuid-42, got %q", acct.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestUpdateAccount
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestUpdateAccount(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"id": "acct-uuid-42",
|
||||
"username": "carol",
|
||||
"account_type": "human",
|
||||
"status": "disabled",
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-02-01T00:00:00Z",
|
||||
"totp_enabled": false,
|
||||
})
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
acct, err := c.UpdateAccount("acct-uuid-42", "disabled")
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateAccount: %v", err)
|
||||
}
|
||||
if acct.Status != "disabled" {
|
||||
t.Errorf("expected status=disabled, got %q", acct.Status)
|
||||
if err := c.UpdateAccount("acct-uuid-42", "inactive"); err != nil {
|
||||
t.Fatalf("UpdateAccount: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestDeleteAccount
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestDeleteAccount(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
@@ -462,16 +560,33 @@ func TestDeleteAccount(t *testing.T) {
|
||||
t.Fatalf("DeleteAccount: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestGetRoles
|
||||
// TestAdminSetPassword
|
||||
// ---------------------------------------------------------------------------
|
||||
func TestGetRoles(t *testing.T) {
|
||||
|
||||
func TestAdminSetPassword(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
if r.Method != http.MethodPut || !strings.HasSuffix(r.URL.Path, "/password") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(r.URL.Path, "/roles") {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
if err := c.AdminSetPassword("acct-uuid-42", "new-s3cr3t-long"); err != nil {
|
||||
t.Fatalf("AdminSetPassword: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestGetRoles / TestSetRoles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetRoles(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || !strings.HasSuffix(r.URL.Path, "/roles") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
@@ -492,9 +607,7 @@ func TestGetRoles(t *testing.T) {
|
||||
t.Errorf("expected roles[0]=admin, got %q", roles[0])
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestSetRoles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSetRoles(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
@@ -509,9 +622,79 @@ func TestSetRoles(t *testing.T) {
|
||||
t.Fatalf("SetRoles: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestIssueServiceToken
|
||||
// TestGetAccountTags / TestSetAccountTags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetAccountTags(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || !strings.HasSuffix(r.URL.Path, "/tags") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"tags": []string{"env:production", "svc:payments-api"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
tags, err := c.GetAccountTags("acct-uuid-42")
|
||||
if err != nil {
|
||||
t.Fatalf("GetAccountTags: %v", err)
|
||||
}
|
||||
if len(tags) != 2 {
|
||||
t.Errorf("expected 2 tags, got %d", len(tags))
|
||||
}
|
||||
if tags[0] != "env:production" {
|
||||
t.Errorf("expected tags[0]=env:production, got %q", tags[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAccountTags(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut || !strings.HasSuffix(r.URL.Path, "/tags") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"tags": []string{"env:staging"},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
tags, err := c.SetAccountTags("acct-uuid-42", []string{"env:staging"})
|
||||
if err != nil {
|
||||
t.Fatalf("SetAccountTags: unexpected error: %v", err)
|
||||
}
|
||||
if len(tags) != 1 || tags[0] != "env:staging" {
|
||||
t.Errorf("expected [env:staging], got %v", tags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAccountTagsClear(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"tags": []string{}})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
tags, err := c.SetAccountTags("acct-uuid-42", []string{})
|
||||
if err != nil {
|
||||
t.Fatalf("SetAccountTags (clear): unexpected error: %v", err)
|
||||
}
|
||||
if len(tags) != 0 {
|
||||
t.Errorf("expected empty tags, got %v", tags)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestIssueServiceToken / TestRevokeToken
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestIssueServiceToken(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/token/issue" || r.Method != http.MethodPost {
|
||||
@@ -519,8 +702,7 @@ func TestIssueServiceToken(t *testing.T) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"token": "svc-tok-xyz",
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"token": "svc-tok-xyz", "expires_at": "2099-01-01T00:00:00Z",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -536,16 +718,10 @@ func TestIssueServiceToken(t *testing.T) {
|
||||
t.Error("expected non-empty expires_at")
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestRevokeToken
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestRevokeToken(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(r.URL.Path, "/v1/token/") {
|
||||
if r.Method != http.MethodDelete || !strings.HasPrefix(r.URL.Path, "/v1/token/") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
@@ -557,25 +733,20 @@ func TestRevokeToken(t *testing.T) {
|
||||
t.Fatalf("RevokeToken: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestGetPGCreds
|
||||
// TestGetPGCreds / TestSetPGCreds
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetPGCreds(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(r.URL.Path, "/pgcreds") {
|
||||
if r.Method != http.MethodGet || !strings.HasSuffix(r.URL.Path, "/pgcreds") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"host": "db.example.com",
|
||||
"port": 5432,
|
||||
"database": "myapp",
|
||||
"username": "appuser",
|
||||
"password": "secretpw",
|
||||
"host": "db.example.com", "port": 5432,
|
||||
"database": "myapp", "username": "appuser", "password": "secretpw",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -594,16 +765,10 @@ func TestGetPGCreds(t *testing.T) {
|
||||
t.Errorf("expected password=secretpw, got %q", creds.Password)
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestSetPGCreds
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestSetPGCreds(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(r.URL.Path, "/pgcreds") {
|
||||
if r.Method != http.MethodPut || !strings.HasSuffix(r.URL.Path, "/pgcreds") {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
@@ -611,14 +776,238 @@ func TestSetPGCreds(t *testing.T) {
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
err := c.SetPGCreds("acct-uuid-42", "db.example.com", 5432, "myapp", "appuser", "secretpw")
|
||||
if err != nil {
|
||||
if err := c.SetPGCreds("acct-uuid-42", "db.example.com", 5432, "myapp", "appuser", "secretpw"); err != nil {
|
||||
t.Fatalf("SetPGCreds: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestListAudit
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestListAudit(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasPrefix(r.URL.Path, "/v1/audit") || r.Method != http.MethodGet {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"events": []map[string]interface{}{
|
||||
{"id": 42, "event_type": "login_ok", "event_time": "2026-03-11T09:01:23Z",
|
||||
"actor_id": "acct-uuid-1", "ip_address": "192.0.2.1"},
|
||||
},
|
||||
"total": 1, "limit": 50, "offset": 0,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
resp, err := c.ListAudit(mciasgoclient.AuditFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListAudit: %v", err)
|
||||
}
|
||||
if resp.Total != 1 {
|
||||
t.Errorf("expected total=1, got %d", resp.Total)
|
||||
}
|
||||
if len(resp.Events) != 1 {
|
||||
t.Fatalf("expected 1 event, got %d", len(resp.Events))
|
||||
}
|
||||
if resp.Events[0].EventType != "login_ok" {
|
||||
t.Errorf("expected event_type=login_ok, got %q", resp.Events[0].EventType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAuditWithFilter(t *testing.T) {
|
||||
var capturedQuery string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedQuery = r.URL.RawQuery
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"events": []map[string]interface{}{},
|
||||
"total": 0, "limit": 10, "offset": 5,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
_, err := c.ListAudit(mciasgoclient.AuditFilter{
|
||||
Limit: 10, Offset: 5, EventType: "login_fail", ActorID: "acct-uuid-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListAudit: %v", err)
|
||||
}
|
||||
for _, want := range []string{"limit=10", "offset=5", "event_type=login_fail", "actor_id=acct-uuid-1"} {
|
||||
if !strings.Contains(capturedQuery, want) {
|
||||
t.Errorf("expected %q in query string, got %q", want, capturedQuery)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestListPolicyRules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestListPolicyRules(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/policy/rules" || r.Method != http.MethodGet {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, []map[string]interface{}{
|
||||
{
|
||||
"id": 1, "priority": 100,
|
||||
"description": "Allow payments-api to read its own pgcreds",
|
||||
"rule": map[string]interface{}{"effect": "allow", "actions": []string{"pgcreds:read"}},
|
||||
"enabled": true,
|
||||
"created_at": "2026-03-11T09:00:00Z", "updated_at": "2026-03-11T09:00:00Z",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
rules, err := c.ListPolicyRules()
|
||||
if err != nil {
|
||||
t.Fatalf("ListPolicyRules: %v", err)
|
||||
}
|
||||
if len(rules) != 1 {
|
||||
t.Fatalf("expected 1 rule, got %d", len(rules))
|
||||
}
|
||||
if rules[0].ID != 1 {
|
||||
t.Errorf("expected id=1, got %d", rules[0].ID)
|
||||
}
|
||||
if rules[0].Description != "Allow payments-api to read its own pgcreds" {
|
||||
t.Errorf("unexpected description: %q", rules[0].Description)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestCreatePolicyRule
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestCreatePolicyRule(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/policy/rules" || r.Method != http.MethodPost {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": 7, "priority": 50, "description": "Test rule",
|
||||
"rule": map[string]interface{}{"effect": "deny"},
|
||||
"enabled": true,
|
||||
"created_at": "2026-03-11T09:00:00Z", "updated_at": "2026-03-11T09:00:00Z",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
rule, err := c.CreatePolicyRule(mciasgoclient.CreatePolicyRuleRequest{
|
||||
Description: "Test rule",
|
||||
Priority: 50,
|
||||
Rule: mciasgoclient.PolicyRuleBody{Effect: "deny"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePolicyRule: %v", err)
|
||||
}
|
||||
if rule.ID != 7 {
|
||||
t.Errorf("expected id=7, got %d", rule.ID)
|
||||
}
|
||||
if rule.Priority != 50 {
|
||||
t.Errorf("expected priority=50, got %d", rule.Priority)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestGetPolicyRule
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestGetPolicyRule(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/v1/policy/rules/7" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"id": 7, "priority": 50, "description": "Test rule",
|
||||
"rule": map[string]interface{}{"effect": "allow"},
|
||||
"enabled": true,
|
||||
"created_at": "2026-03-11T09:00:00Z", "updated_at": "2026-03-11T09:00:00Z",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
rule, err := c.GetPolicyRule(7)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPolicyRule: %v", err)
|
||||
}
|
||||
if rule.ID != 7 {
|
||||
t.Errorf("expected id=7, got %d", rule.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPolicyRuleNotFound(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusNotFound, "rule not found")
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
_, err := c.GetPolicyRule(999)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 404")
|
||||
}
|
||||
var notFoundErr *mciasgoclient.MciasNotFoundError
|
||||
if !errors.As(err, ¬FoundErr) {
|
||||
t.Errorf("expected MciasNotFoundError, got %T: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestUpdatePolicyRule
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestUpdatePolicyRule(t *testing.T) {
|
||||
enabled := false
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPatch || r.URL.Path != "/v1/policy/rules/7" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"id": 7, "priority": 50, "description": "Test rule",
|
||||
"rule": map[string]interface{}{"effect": "allow"},
|
||||
"enabled": false,
|
||||
"created_at": "2026-03-11T09:00:00Z", "updated_at": "2026-03-12T10:00:00Z",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
rule, err := c.UpdatePolicyRule(7, mciasgoclient.UpdatePolicyRuleRequest{Enabled: &enabled})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePolicyRule: %v", err)
|
||||
}
|
||||
if rule.Enabled {
|
||||
t.Error("expected enabled=false after update")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestDeletePolicyRule
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestDeletePolicyRule(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete || r.URL.Path != "/v1/policy/rules/7" {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
if err := c.DeletePolicyRule(7); err != nil {
|
||||
t.Fatalf("DeletePolicyRule: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TestIntegration: full login → validate → logout flow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
const sessionToken = "integration-tok-999"
|
||||
mux := http.NewServeMux()
|
||||
@@ -640,8 +1029,7 @@ func TestIntegration(t *testing.T) {
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"token": sessionToken,
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"token": sessionToken, "expires_at": "2099-01-01T00:00:00Z",
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/v1/token/validate", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -658,15 +1046,11 @@ func TestIntegration(t *testing.T) {
|
||||
}
|
||||
if body.Token == sessionToken {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"valid": true,
|
||||
"sub": "alice-uuid",
|
||||
"roles": []string{"user"},
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"valid": true, "sub": "alice-uuid",
|
||||
"roles": []string{"user"}, "expires_at": "2099-01-01T00:00:00Z",
|
||||
})
|
||||
} else {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"valid": false,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"valid": false})
|
||||
}
|
||||
})
|
||||
mux.HandleFunc("/v1/auth/logout", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -674,9 +1058,7 @@ func TestIntegration(t *testing.T) {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
// Verify Authorization header is present.
|
||||
auth := r.Header.Get("Authorization")
|
||||
if auth == "" {
|
||||
if r.Header.Get("Authorization") == "" {
|
||||
writeError(w, http.StatusUnauthorized, "missing token")
|
||||
return
|
||||
}
|
||||
@@ -685,7 +1067,8 @@ func TestIntegration(t *testing.T) {
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
c := newTestClient(t, srv.URL)
|
||||
// Step 1: login with wrong credentials should fail.
|
||||
|
||||
// Step 1: wrong credentials → MciasAuthError.
|
||||
_, _, err := c.Login("alice", "wrong-password", "")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for wrong credentials")
|
||||
@@ -694,7 +1077,8 @@ func TestIntegration(t *testing.T) {
|
||||
if !errors.As(err, &authErr) {
|
||||
t.Errorf("expected MciasAuthError, got %T", err)
|
||||
}
|
||||
// Step 2: login with correct credentials.
|
||||
|
||||
// Step 2: correct login.
|
||||
tok, _, err := c.Login("alice", "correct-horse", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Login: %v", err)
|
||||
@@ -702,7 +1086,8 @@ func TestIntegration(t *testing.T) {
|
||||
if tok != sessionToken {
|
||||
t.Errorf("expected %q, got %q", sessionToken, tok)
|
||||
}
|
||||
// Step 3: validate the returned token.
|
||||
|
||||
// Step 3: validate → valid=true.
|
||||
claims, err := c.ValidateToken(tok)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken: %v", err)
|
||||
@@ -713,7 +1098,8 @@ func TestIntegration(t *testing.T) {
|
||||
if claims.Sub != "alice-uuid" {
|
||||
t.Errorf("expected sub=alice-uuid, got %q", claims.Sub)
|
||||
}
|
||||
// Step 4: validate an unknown token returns Valid=false, not an error.
|
||||
|
||||
// Step 4: garbage token → valid=false (not an error).
|
||||
claims2, err := c.ValidateToken("garbage-token")
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken(garbage): unexpected error: %v", err)
|
||||
@@ -721,7 +1107,8 @@ func TestIntegration(t *testing.T) {
|
||||
if claims2.Valid {
|
||||
t.Error("expected Valid=false for garbage token")
|
||||
}
|
||||
// Step 5: logout clears the stored token.
|
||||
|
||||
// Step 5: logout clears stored token.
|
||||
if err := c.Logout(); err != nil {
|
||||
t.Fatalf("Logout: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user