Compare commits
1 Commits
unit11-git
...
unit8-grpc
| Author | SHA1 | Date | |
|---|---|---|---|
| 82b7d295ef |
815
internal/grpcserver/handlers_test.go
Normal file
815
internal/grpcserver/handlers_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user