From 82b7d295effc2c2d1c14e1303b04b86ca70ccef2 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Thu, 26 Mar 2026 21:06:44 -0700 Subject: [PATCH] Add gRPC handler tests for zones, records, admin, and interceptors Full integration tests exercising gRPC services through real server with mock MCIAS auth. Covers all CRUD operations for zones and records, health check bypass, auth/admin interceptor enforcement, CNAME exclusivity conflicts, and method map completeness verification. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/grpcserver/handlers_test.go | 815 +++++++++++++++++++++++++++ 1 file changed, 815 insertions(+) create mode 100644 internal/grpcserver/handlers_test.go diff --git a/internal/grpcserver/handlers_test.go b/internal/grpcserver/handlers_test.go new file mode 100644 index 0000000..9104006 --- /dev/null +++ b/internal/grpcserver/handlers_test.go @@ -0,0 +1,815 @@ +package grpcserver + +import ( + "context" + "encoding/json" + "log/slog" + "net" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth" + + pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1" + "git.wntrmute.dev/kyle/mcns/internal/db" +) + +// mockMCIAS starts a fake MCIAS HTTP server for token validation. +// Recognized tokens: +// - "admin-token" -> valid, username=admin-uuid, roles=[admin] +// - "user-token" -> valid, username=user-uuid, roles=[user] +// - anything else -> invalid +func mockMCIAS(t *testing.T) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + mux.HandleFunc("POST /v1/token/validate", func(w http.ResponseWriter, r *http.Request) { + var req struct { + Token string `json:"token"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request", http.StatusBadRequest) + return + } + w.Header().Set("Content-Type", "application/json") + switch req.Token { + case "admin-token": + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "valid": true, + "username": "admin-uuid", + "account_type": "human", + "roles": []string{"admin"}, + }) + case "user-token": + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "valid": true, + "username": "user-uuid", + "account_type": "human", + "roles": []string{"user"}, + }) + default: + _ = json.NewEncoder(w).Encode(map[string]interface{}{"valid": false}) + } + }) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + return srv +} + +// testAuthenticator creates an mcdsl/auth.Authenticator that talks to the given mock MCIAS. +func testAuthenticator(t *testing.T, serverURL string) *mcdslauth.Authenticator { + t.Helper() + a, err := mcdslauth.New(mcdslauth.Config{ServerURL: serverURL}, slog.Default()) + if err != nil { + t.Fatalf("auth.New: %v", err) + } + return a +} + +// openTestDB creates a temporary test database with migrations applied. +func openTestDB(t *testing.T) *db.DB { + t.Helper() + path := filepath.Join(t.TempDir(), "test.db") + d, err := db.Open(path) + if err != nil { + t.Fatalf("Open: %v", err) + } + t.Cleanup(func() { _ = d.Close() }) + if err := d.Migrate(); err != nil { + t.Fatalf("Migrate: %v", err) + } + return d +} + +// startTestServer creates a gRPC server with auth interceptors and returns +// a connected client. Passing empty cert/key strings skips TLS. +func startTestServer(t *testing.T, deps Deps) *grpc.ClientConn { + t.Helper() + + srv, err := New("", "", deps, slog.Default()) + if err != nil { + t.Fatalf("New: %v", err) + } + + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("Listen: %v", err) + } + + go func() { + _ = srv.Serve(lis) + }() + t.Cleanup(func() { srv.GracefulStop() }) + + //nolint:gosec // insecure credentials for testing only + cc, err := grpc.NewClient( + lis.Addr().String(), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("Dial: %v", err) + } + t.Cleanup(func() { _ = cc.Close() }) + + return cc +} + +// withAuth adds a bearer token to the outgoing context metadata. +func withAuth(ctx context.Context, token string) context.Context { + return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token) +} + +// seedZone creates a test zone and returns it. +func seedZone(t *testing.T, database *db.DB, name string) *db.Zone { + t.Helper() + zone, err := database.CreateZone(name, "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300) + if err != nil { + t.Fatalf("seed zone %q: %v", name, err) + } + return zone +} + +// seedRecord creates a test A record and returns it. +func seedRecord(t *testing.T, database *db.DB, zoneName, name, value string) *db.Record { + t.Helper() + rec, err := database.CreateRecord(zoneName, name, "A", value, 300) + if err != nil { + t.Fatalf("seed record %s.%s: %v", name, zoneName, err) + } + return rec +} + +// --------------------------------------------------------------------------- +// Admin tests +// --------------------------------------------------------------------------- + +func TestHealthBypassesAuth(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + client := pb.NewAdminServiceClient(cc) + + // No auth token -- should still succeed because Health is public. + resp, err := client.Health(context.Background(), &pb.HealthRequest{}) + if err != nil { + t.Fatalf("Health should not require auth: %v", err) + } + if resp.Status != "ok" { + t.Fatalf("Health status: got %q, want %q", resp.Status, "ok") + } +} + +// --------------------------------------------------------------------------- +// Zone tests +// --------------------------------------------------------------------------- + +func TestListZones(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "user-token") + client := pb.NewZoneServiceClient(cc) + + resp, err := client.ListZones(ctx, &pb.ListZonesRequest{}) + if err != nil { + t.Fatalf("ListZones: %v", err) + } + // Seed migration creates 2 zones. + if len(resp.Zones) != 2 { + t.Fatalf("got %d zones, want 2 (seed zones)", len(resp.Zones)) + } +} + +func TestGetZoneFound(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "user-token") + client := pb.NewZoneServiceClient(cc) + + zone, err := client.GetZone(ctx, &pb.GetZoneRequest{Name: "example.com"}) + if err != nil { + t.Fatalf("GetZone: %v", err) + } + if zone.Name != "example.com" { + t.Fatalf("got name %q, want %q", zone.Name, "example.com") + } +} + +func TestGetZoneNotFound(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "user-token") + client := pb.NewZoneServiceClient(cc) + + _, err := client.GetZone(ctx, &pb.GetZoneRequest{Name: "nonexistent.com"}) + if err == nil { + t.Fatal("expected error for nonexistent zone") + } + st, ok := status.FromError(err) + if !ok { + t.Fatalf("expected gRPC status, got %v", err) + } + if st.Code() != codes.NotFound { + t.Fatalf("code: got %v, want NotFound", st.Code()) + } +} + +func TestCreateZoneSuccess(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewZoneServiceClient(cc) + + zone, err := client.CreateZone(ctx, &pb.CreateZoneRequest{ + Name: "newzone.com", + PrimaryNs: "ns1.newzone.com.", + AdminEmail: "admin.newzone.com.", + Refresh: 3600, + Retry: 600, + Expire: 86400, + MinimumTtl: 300, + }) + if err != nil { + t.Fatalf("CreateZone: %v", err) + } + if zone.Name != "newzone.com" { + t.Fatalf("got name %q, want %q", zone.Name, "newzone.com") + } + if zone.Serial == 0 { + t.Fatal("serial should not be zero") + } +} + +func TestCreateZoneDuplicate(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewZoneServiceClient(cc) + + _, err := client.CreateZone(ctx, &pb.CreateZoneRequest{ + Name: "example.com", + PrimaryNs: "ns1.example.com.", + AdminEmail: "admin.example.com.", + }) + if err == nil { + t.Fatal("expected error for duplicate zone") + } + st, ok := status.FromError(err) + if !ok { + t.Fatalf("expected gRPC status, got %v", err) + } + if st.Code() != codes.AlreadyExists { + t.Fatalf("code: got %v, want AlreadyExists", st.Code()) + } +} + +func TestUpdateZone(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + original := seedZone(t, database, "example.com") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewZoneServiceClient(cc) + + updated, err := client.UpdateZone(ctx, &pb.UpdateZoneRequest{ + Name: "example.com", + PrimaryNs: "ns2.example.com.", + AdminEmail: "newadmin.example.com.", + Refresh: 7200, + Retry: 1200, + Expire: 172800, + MinimumTtl: 600, + }) + if err != nil { + t.Fatalf("UpdateZone: %v", err) + } + if updated.PrimaryNs != "ns2.example.com." { + t.Fatalf("got primary_ns %q, want %q", updated.PrimaryNs, "ns2.example.com.") + } + if updated.Serial <= original.Serial { + t.Fatalf("serial should have incremented: %d <= %d", updated.Serial, original.Serial) + } +} + +func TestDeleteZone(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewZoneServiceClient(cc) + + _, err := client.DeleteZone(ctx, &pb.DeleteZoneRequest{Name: "example.com"}) + if err != nil { + t.Fatalf("DeleteZone: %v", err) + } + + // Verify it is gone. + _, err = client.GetZone(withAuth(context.Background(), "user-token"), &pb.GetZoneRequest{Name: "example.com"}) + if err == nil { + t.Fatal("expected NotFound after delete") + } + st, _ := status.FromError(err) + if st.Code() != codes.NotFound { + t.Fatalf("code: got %v, want NotFound", st.Code()) + } +} + +func TestDeleteZoneNotFound(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewZoneServiceClient(cc) + + _, err := client.DeleteZone(ctx, &pb.DeleteZoneRequest{Name: "nonexistent.com"}) + if err == nil { + t.Fatal("expected error for nonexistent zone") + } + st, _ := status.FromError(err) + if st.Code() != codes.NotFound { + t.Fatalf("code: got %v, want NotFound", st.Code()) + } +} + +// --------------------------------------------------------------------------- +// Record tests +// --------------------------------------------------------------------------- + +func TestListRecords(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + seedRecord(t, database, "example.com", "www", "10.0.0.1") + seedRecord(t, database, "example.com", "mail", "10.0.0.2") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "user-token") + client := pb.NewRecordServiceClient(cc) + + resp, err := client.ListRecords(ctx, &pb.ListRecordsRequest{Zone: "example.com"}) + if err != nil { + t.Fatalf("ListRecords: %v", err) + } + if len(resp.Records) != 2 { + t.Fatalf("got %d records, want 2", len(resp.Records)) + } +} + +func TestGetRecordFound(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + rec := seedRecord(t, database, "example.com", "www", "10.0.0.1") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "user-token") + client := pb.NewRecordServiceClient(cc) + + got, err := client.GetRecord(ctx, &pb.GetRecordRequest{Id: rec.ID}) + if err != nil { + t.Fatalf("GetRecord: %v", err) + } + if got.Name != "www" { + t.Fatalf("got name %q, want %q", got.Name, "www") + } + if got.Value != "10.0.0.1" { + t.Fatalf("got value %q, want %q", got.Value, "10.0.0.1") + } +} + +func TestGetRecordNotFound(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "user-token") + client := pb.NewRecordServiceClient(cc) + + _, err := client.GetRecord(ctx, &pb.GetRecordRequest{Id: 999999}) + if err == nil { + t.Fatal("expected error for nonexistent record") + } + st, _ := status.FromError(err) + if st.Code() != codes.NotFound { + t.Fatalf("code: got %v, want NotFound", st.Code()) + } +} + +func TestCreateRecordSuccess(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewRecordServiceClient(cc) + + rec, err := client.CreateRecord(ctx, &pb.CreateRecordRequest{ + Zone: "example.com", + Name: "www", + Type: "A", + Value: "10.0.0.1", + Ttl: 300, + }) + if err != nil { + t.Fatalf("CreateRecord: %v", err) + } + if rec.Name != "www" { + t.Fatalf("got name %q, want %q", rec.Name, "www") + } + if rec.Type != "A" { + t.Fatalf("got type %q, want %q", rec.Type, "A") + } +} + +func TestCreateRecordInvalidValue(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewRecordServiceClient(cc) + + _, err := client.CreateRecord(ctx, &pb.CreateRecordRequest{ + Zone: "example.com", + Name: "www", + Type: "A", + Value: "not-an-ip", + Ttl: 300, + }) + if err == nil { + t.Fatal("expected error for invalid A record value") + } + st, _ := status.FromError(err) + if st.Code() != codes.InvalidArgument { + t.Fatalf("code: got %v, want InvalidArgument", st.Code()) + } +} + +func TestCreateRecordCNAMEConflict(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + seedRecord(t, database, "example.com", "www", "10.0.0.1") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewRecordServiceClient(cc) + + // Try to create a CNAME for "www" which already has an A record. + _, err := client.CreateRecord(ctx, &pb.CreateRecordRequest{ + Zone: "example.com", + Name: "www", + Type: "CNAME", + Value: "other.example.com.", + Ttl: 300, + }) + if err == nil { + t.Fatal("expected error for CNAME conflict with existing A record") + } + st, _ := status.FromError(err) + if st.Code() != codes.AlreadyExists { + t.Fatalf("code: got %v, want AlreadyExists", st.Code()) + } +} + +func TestCreateRecordAConflictWithCNAME(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + + // Create a CNAME first. + _, err := database.CreateRecord("example.com", "alias", "CNAME", "target.example.com.", 300) + if err != nil { + t.Fatalf("seed CNAME: %v", err) + } + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewRecordServiceClient(cc) + + // Try to create an A record for "alias" which already has a CNAME. + _, err = client.CreateRecord(ctx, &pb.CreateRecordRequest{ + Zone: "example.com", + Name: "alias", + Type: "A", + Value: "10.0.0.1", + Ttl: 300, + }) + if err == nil { + t.Fatal("expected error for A record conflict with existing CNAME") + } + st, _ := status.FromError(err) + if st.Code() != codes.AlreadyExists { + t.Fatalf("code: got %v, want AlreadyExists", st.Code()) + } +} + +func TestUpdateRecord(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + rec := seedRecord(t, database, "example.com", "www", "10.0.0.1") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewRecordServiceClient(cc) + + updated, err := client.UpdateRecord(ctx, &pb.UpdateRecordRequest{ + Id: rec.ID, + Name: "www", + Type: "A", + Value: "10.0.0.2", + Ttl: 600, + }) + if err != nil { + t.Fatalf("UpdateRecord: %v", err) + } + if updated.Value != "10.0.0.2" { + t.Fatalf("got value %q, want %q", updated.Value, "10.0.0.2") + } + if updated.Ttl != 600 { + t.Fatalf("got ttl %d, want 600", updated.Ttl) + } +} + +func TestUpdateRecordNotFound(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewRecordServiceClient(cc) + + _, err := client.UpdateRecord(ctx, &pb.UpdateRecordRequest{ + Id: 999999, + Name: "www", + Type: "A", + Value: "10.0.0.1", + Ttl: 300, + }) + if err == nil { + t.Fatal("expected error for nonexistent record") + } + st, _ := status.FromError(err) + if st.Code() != codes.NotFound { + t.Fatalf("code: got %v, want NotFound", st.Code()) + } +} + +func TestDeleteRecord(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + seedZone(t, database, "example.com") + rec := seedRecord(t, database, "example.com", "www", "10.0.0.1") + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewRecordServiceClient(cc) + + _, err := client.DeleteRecord(ctx, &pb.DeleteRecordRequest{Id: rec.ID}) + if err != nil { + t.Fatalf("DeleteRecord: %v", err) + } + + // Verify it is gone. + _, err = client.GetRecord(withAuth(context.Background(), "user-token"), &pb.GetRecordRequest{Id: rec.ID}) + if err == nil { + t.Fatal("expected NotFound after delete") + } + st, _ := status.FromError(err) + if st.Code() != codes.NotFound { + t.Fatalf("code: got %v, want NotFound", st.Code()) + } +} + +func TestDeleteRecordNotFound(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewRecordServiceClient(cc) + + _, err := client.DeleteRecord(ctx, &pb.DeleteRecordRequest{Id: 999999}) + if err == nil { + t.Fatal("expected error for nonexistent record") + } + st, _ := status.FromError(err) + if st.Code() != codes.NotFound { + t.Fatalf("code: got %v, want NotFound", st.Code()) + } +} + +// --------------------------------------------------------------------------- +// Auth interceptor tests +// --------------------------------------------------------------------------- + +func TestAuthRequiredNoToken(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + client := pb.NewZoneServiceClient(cc) + + // No auth token on an auth-required method. + _, err := client.ListZones(context.Background(), &pb.ListZonesRequest{}) + if err == nil { + t.Fatal("expected error for unauthenticated request") + } + st, ok := status.FromError(err) + if !ok { + t.Fatalf("expected gRPC status error, got %v", err) + } + if st.Code() != codes.Unauthenticated { + t.Fatalf("code: got %v, want Unauthenticated", st.Code()) + } +} + +func TestAuthRequiredInvalidToken(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "bad-token") + client := pb.NewZoneServiceClient(cc) + + _, err := client.ListZones(ctx, &pb.ListZonesRequest{}) + if err == nil { + t.Fatal("expected error for invalid token") + } + st, ok := status.FromError(err) + if !ok { + t.Fatalf("expected gRPC status error, got %v", err) + } + if st.Code() != codes.Unauthenticated { + t.Fatalf("code: got %v, want Unauthenticated", st.Code()) + } +} + +func TestAdminRequiredDeniedForUser(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "user-token") + client := pb.NewZoneServiceClient(cc) + + // CreateZone requires admin. + _, err := client.CreateZone(ctx, &pb.CreateZoneRequest{ + Name: "forbidden.com", + PrimaryNs: "ns1.forbidden.com.", + AdminEmail: "admin.forbidden.com.", + }) + if err == nil { + t.Fatal("expected error for non-admin user") + } + st, ok := status.FromError(err) + if !ok { + t.Fatalf("expected gRPC status error, got %v", err) + } + if st.Code() != codes.PermissionDenied { + t.Fatalf("code: got %v, want PermissionDenied", st.Code()) + } +} + +func TestAdminRequiredAllowedForAdmin(t *testing.T) { + mcias := mockMCIAS(t) + auth := testAuthenticator(t, mcias.URL) + database := openTestDB(t) + + cc := startTestServer(t, Deps{DB: database, Authenticator: auth}) + ctx := withAuth(context.Background(), "admin-token") + client := pb.NewZoneServiceClient(cc) + + // Admin should be able to create zones. + zone, err := client.CreateZone(ctx, &pb.CreateZoneRequest{ + Name: "admin-created.com", + PrimaryNs: "ns1.admin-created.com.", + AdminEmail: "admin.admin-created.com.", + }) + if err != nil { + t.Fatalf("CreateZone as admin: %v", err) + } + if zone.Name != "admin-created.com" { + t.Fatalf("got name %q, want %q", zone.Name, "admin-created.com") + } +} + +// --------------------------------------------------------------------------- +// Interceptor map completeness test +// --------------------------------------------------------------------------- + +func TestMethodMapCompleteness(t *testing.T) { + mm := methodMap() + + expectedPublic := []string{ + "/mcns.v1.AdminService/Health", + "/mcns.v1.AuthService/Login", + } + for _, method := range expectedPublic { + if !mm.Public[method] { + t.Errorf("method %s should be public but is not in Public", method) + } + } + if len(mm.Public) != len(expectedPublic) { + t.Errorf("Public has %d entries, expected %d", len(mm.Public), len(expectedPublic)) + } + + expectedAuth := []string{ + "/mcns.v1.AuthService/Logout", + "/mcns.v1.ZoneService/ListZones", + "/mcns.v1.ZoneService/GetZone", + "/mcns.v1.RecordService/ListRecords", + "/mcns.v1.RecordService/GetRecord", + } + for _, method := range expectedAuth { + if !mm.AuthRequired[method] { + t.Errorf("method %s should require auth but is not in AuthRequired", method) + } + } + if len(mm.AuthRequired) != len(expectedAuth) { + t.Errorf("AuthRequired has %d entries, expected %d", len(mm.AuthRequired), len(expectedAuth)) + } + + expectedAdmin := []string{ + "/mcns.v1.ZoneService/CreateZone", + "/mcns.v1.ZoneService/UpdateZone", + "/mcns.v1.ZoneService/DeleteZone", + "/mcns.v1.RecordService/CreateRecord", + "/mcns.v1.RecordService/UpdateRecord", + "/mcns.v1.RecordService/DeleteRecord", + } + for _, method := range expectedAdmin { + if !mm.AdminRequired[method] { + t.Errorf("method %s should require admin but is not in AdminRequired", method) + } + } + if len(mm.AdminRequired) != len(expectedAdmin) { + t.Errorf("AdminRequired has %d entries, expected %d", len(mm.AdminRequired), len(expectedAdmin)) + } + + // Verify no method appears in multiple maps (each RPC in exactly one map). + all := make(map[string]int) + for k := range mm.Public { + all[k]++ + } + for k := range mm.AuthRequired { + all[k]++ + } + for k := range mm.AdminRequired { + all[k]++ + } + for method, count := range all { + if count != 1 { + t.Errorf("method %s appears in %d maps, expected exactly 1", method, count) + } + } +}