@marcel91200 / caddy Mirror · read-only / commits / d7621fdb
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 12, 03:15 UTC

tests: tests for error handling & metrics in admin endpoints (#6805)

* feat/tests: tests for error handling & metrics in admin endpoints - TestAdminHandlerErrorHandling - Tests the handler.handleError() functionality by directly verifying error response formatting - TestAdminHandlerBuiltinRouteErrors - Tests the error handling pathway by using real admin server routes and verifying both error responses and prometheus metrics increments - provisionAdminRouters: add unit tests for admin handler registration and routing for admin.api - TestAllowedOriginsUnixSocket: checks unix socket with default origins are added - TestReplaceRemoteAdminServer: test for replaceRemoteAdminServer with certificate validation, custom origins and cleanup * test: added test for manage manageIdentity --------- Co-authored-by: Mohammed Al Sahaf <msaa1990@gmail.com>

Gaurav Dhameeja committed Feb 12, 2025 at 15:39 UTC d7621fdbe65fc084a257e569d1add39dc9a0ce09
1 file changed +734
admin_test.go
+734
@@ -15,12 +15,19 @@
15 package caddy
16
17 import (
18 + "context"
19 + "crypto/x509"
20 "encoding/json"
21 "fmt"
22 "net/http"
23 + "net/http/httptest"
24 "reflect"
25 "sync"
26 "testing"
27 +
28 + "github.com/caddyserver/certmagic"
29 + "github.com/prometheus/client_golang/prometheus"
30 + dto "github.com/prometheus/client_model/go"
31 )
32
33 var testCfg = []byte(`{
@@ -203,3 +210,730 @@ func BenchmarkLoad(b *testing.B) {
210 Load(testCfg, true)
211 }
212 }
213 +
214 +func TestAdminHandlerErrorHandling(t *testing.T) {
215 + initAdminMetrics()
216 +
217 + handler := adminHandler{
218 + mux: http.NewServeMux(),
219 + }
220 +
221 + handler.mux.Handle("/error", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
222 + err := fmt.Errorf("test error")
223 + handler.handleError(w, r, err)
224 + }))
225 +
226 + req := httptest.NewRequest(http.MethodGet, "/error", nil)
227 + rr := httptest.NewRecorder()
228 +
229 + handler.ServeHTTP(rr, req)
230 +
231 + if rr.Code == http.StatusOK {
232 + t.Error("expected error response, got success")
233 + }
234 +
235 + var apiErr APIError
236 + if err := json.NewDecoder(rr.Body).Decode(&apiErr); err != nil {
237 + t.Fatalf("decoding response: %v", err)
238 + }
239 + if apiErr.Message != "test error" {
240 + t.Errorf("expected error message 'test error', got '%s'", apiErr.Message)
241 + }
242 +}
243 +
244 +func initAdminMetrics() {
245 + if adminMetrics.requestErrors != nil {
246 + prometheus.Unregister(adminMetrics.requestErrors)
247 + }
248 + if adminMetrics.requestCount != nil {
249 + prometheus.Unregister(adminMetrics.requestCount)
250 + }
251 +
252 + adminMetrics.requestErrors = prometheus.NewCounterVec(prometheus.CounterOpts{
253 + Namespace: "caddy",
254 + Subsystem: "admin_http",
255 + Name: "request_errors_total",
256 + Help: "Number of errors that occurred handling admin endpoint requests",
257 + }, []string{"handler", "path", "method"})
258 +
259 + adminMetrics.requestCount = prometheus.NewCounterVec(prometheus.CounterOpts{
260 + Namespace: "caddy",
261 + Subsystem: "admin_http",
262 + Name: "requests_total",
263 + Help: "Count of requests to the admin endpoint",
264 + }, []string{"handler", "path", "code", "method"}) // Added code and method labels
265 +
266 + prometheus.MustRegister(adminMetrics.requestErrors)
267 + prometheus.MustRegister(adminMetrics.requestCount)
268 +}
269 +
270 +func TestAdminHandlerBuiltinRouteErrors(t *testing.T) {
271 + initAdminMetrics()
272 +
273 + cfg := &Config{
274 + Admin: &AdminConfig{
275 + Listen: "localhost:2019",
276 + },
277 + }
278 +
279 + err := replaceLocalAdminServer(cfg, Context{})
280 + if err != nil {
281 + t.Fatalf("setting up admin server: %v", err)
282 + }
283 + defer func() {
284 + stopAdminServer(localAdminServer)
285 + }()
286 +
287 + tests := []struct {
288 + name string
289 + path string
290 + method string
291 + expectedStatus int
292 + }{
293 + {
294 + name: "stop endpoint wrong method",
295 + path: "/stop",
296 + method: http.MethodGet,
297 + expectedStatus: http.StatusMethodNotAllowed,
298 + },
299 + {
300 + name: "config endpoint wrong content-type",
301 + path: "/config/",
302 + method: http.MethodPost,
303 + expectedStatus: http.StatusBadRequest,
304 + },
305 + {
306 + name: "config ID missing ID",
307 + path: "/id/",
308 + method: http.MethodGet,
309 + expectedStatus: http.StatusBadRequest,
310 + },
311 + }
312 +
313 + for _, test := range tests {
314 + t.Run(test.name, func(t *testing.T) {
315 + req := httptest.NewRequest(test.method, fmt.Sprintf("http://localhost:2019%s", test.path), nil)
316 + rr := httptest.NewRecorder()
317 +
318 + localAdminServer.Handler.ServeHTTP(rr, req)
319 +
320 + if rr.Code != test.expectedStatus {
321 + t.Errorf("expected status %d but got %d", test.expectedStatus, rr.Code)
322 + }
323 +
324 + metricValue := testGetMetricValue(map[string]string{
325 + "path": test.path,
326 + "handler": "admin",
327 + "method": test.method,
328 + })
329 + if metricValue != 1 {
330 + t.Errorf("expected error metric to be incremented once, got %v", metricValue)
331 + }
332 + })
333 + }
334 +}
335 +
336 +func testGetMetricValue(labels map[string]string) float64 {
337 + promLabels := prometheus.Labels{}
338 + for k, v := range labels {
339 + promLabels[k] = v
340 + }
341 +
342 + metric, err := adminMetrics.requestErrors.GetMetricWith(promLabels)
343 + if err != nil {
344 + return 0
345 + }
346 +
347 + pb := &dto.Metric{}
348 + metric.Write(pb)
349 + return pb.GetCounter().GetValue()
350 +}
351 +
352 +type mockRouter struct {
353 + routes []AdminRoute
354 +}
355 +
356 +func (m mockRouter) Routes() []AdminRoute {
357 + return m.routes
358 +}
359 +
360 +type mockModule struct {
361 + mockRouter
362 +}
363 +
364 +func (m *mockModule) CaddyModule() ModuleInfo {
365 + return ModuleInfo{
366 + ID: "admin.api.mock",
367 + New: func() Module {
368 + mm := &mockModule{
369 + mockRouter: mockRouter{
370 + routes: m.routes,
371 + },
372 + }
373 + return mm
374 + },
375 + }
376 +}
377 +
378 +func TestNewAdminHandlerRouterRegistration(t *testing.T) {
379 + originalModules := make(map[string]ModuleInfo)
380 + for k, v := range modules {
381 + originalModules[k] = v
382 + }
383 + defer func() {
384 + modules = originalModules
385 + }()
386 +
387 + mockRoute := AdminRoute{
388 + Pattern: "/mock",
389 + Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
390 + w.WriteHeader(http.StatusOK)
391 + return nil
392 + }),
393 + }
394 +
395 + mock := &mockModule{
396 + mockRouter: mockRouter{
397 + routes: []AdminRoute{mockRoute},
398 + },
399 + }
400 + RegisterModule(mock)
401 +
402 + addr, err := ParseNetworkAddress("localhost:2019")
403 + if err != nil {
404 + t.Fatalf("Failed to parse address: %v", err)
405 + }
406 +
407 + admin := &AdminConfig{
408 + EnforceOrigin: false,
409 + }
410 + handler := admin.newAdminHandler(addr, false, Context{})
411 +
412 + req := httptest.NewRequest("GET", "/mock", nil)
413 + req.Host = "localhost:2019"
414 + rr := httptest.NewRecorder()
415 +
416 + handler.ServeHTTP(rr, req)
417 +
418 + if rr.Code != http.StatusOK {
419 + t.Errorf("Expected status code %d but got %d", http.StatusOK, rr.Code)
420 + t.Logf("Response body: %s", rr.Body.String())
421 + }
422 +
423 + if len(admin.routers) != 1 {
424 + t.Errorf("Expected 1 router to be stored, got %d", len(admin.routers))
425 + }
426 +}
427 +
428 +type mockProvisionableRouter struct {
429 + mockRouter
430 + provisionErr error
431 + provisioned bool
432 +}
433 +
434 +func (m *mockProvisionableRouter) Provision(Context) error {
435 + m.provisioned = true
436 + return m.provisionErr
437 +}
438 +
439 +type mockProvisionableModule struct {
440 + *mockProvisionableRouter
441 +}
442 +
443 +func (m *mockProvisionableModule) CaddyModule() ModuleInfo {
444 + return ModuleInfo{
445 + ID: "admin.api.mock_provision",
446 + New: func() Module {
447 + mm := &mockProvisionableModule{
448 + mockProvisionableRouter: &mockProvisionableRouter{
449 + mockRouter: m.mockRouter,
450 + provisionErr: m.provisionErr,
451 + },
452 + }
453 + return mm
454 + },
455 + }
456 +}
457 +
458 +func TestAdminRouterProvisioning(t *testing.T) {
459 + tests := []struct {
460 + name string
461 + provisionErr error
462 + wantErr bool
463 + routersAfter int // expected number of routers after provisioning
464 + }{
465 + {
466 + name: "successful provisioning",
467 + provisionErr: nil,
468 + wantErr: false,
469 + routersAfter: 0,
470 + },
471 + {
472 + name: "provisioning error",
473 + provisionErr: fmt.Errorf("provision failed"),
474 + wantErr: true,
475 + routersAfter: 1,
476 + },
477 + }
478 +
479 + for _, test := range tests {
480 + t.Run(test.name, func(t *testing.T) {
481 + originalModules := make(map[string]ModuleInfo)
482 + for k, v := range modules {
483 + originalModules[k] = v
484 + }
485 + defer func() {
486 + modules = originalModules
487 + }()
488 +
489 + mockRoute := AdminRoute{
490 + Pattern: "/mock",
491 + Handler: AdminHandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
492 + return nil
493 + }),
494 + }
495 +
496 + // Create provisionable module
497 + mock := &mockProvisionableModule{
498 + mockProvisionableRouter: &mockProvisionableRouter{
499 + mockRouter: mockRouter{
500 + routes: []AdminRoute{mockRoute},
501 + },
502 + provisionErr: test.provisionErr,
503 + },
504 + }
505 + RegisterModule(mock)
506 +
507 + admin := &AdminConfig{}
508 + addr, err := ParseNetworkAddress("localhost:2019")
509 + if err != nil {
510 + t.Fatalf("Failed to parse address: %v", err)
511 + }
512 +
513 + _ = admin.newAdminHandler(addr, false, Context{})
514 + err = admin.provisionAdminRouters(Context{})
515 +
516 + if test.wantErr {
517 + if err == nil {
518 + t.Error("Expected error but got nil")
519 + }
520 + } else {
521 + if err != nil {
522 + t.Errorf("Expected no error but got: %v", err)
523 + }
524 + }
525 +
526 + if len(admin.routers) != test.routersAfter {
527 + t.Errorf("Expected %d routers after provisioning, got %d", test.routersAfter, len(admin.routers))
528 + }
529 + })
530 + }
531 +}
532 +
533 +func TestAllowedOriginsUnixSocket(t *testing.T) {
534 + tests := []struct {
535 + name string
536 + addr NetworkAddress
537 + origins []string
538 + expectOrigins []string
539 + }{
540 + {
541 + name: "unix socket with default origins",
542 + addr: NetworkAddress{
543 + Network: "unix",
544 + Host: "/tmp/caddy.sock",
545 + },
546 + origins: nil, // default origins
547 + expectOrigins: []string{
548 + "", // empty host as per RFC 2616
549 + "127.0.0.1",
550 + "::1",
551 + },
552 + },
553 + {
554 + name: "unix socket with custom origins",
555 + addr: NetworkAddress{
556 + Network: "unix",
557 + Host: "/tmp/caddy.sock",
558 + },
559 + origins: []string{"example.com"},
560 + expectOrigins: []string{
561 + "example.com",
562 + },
563 + },
564 + {
565 + name: "tcp socket on localhost gets all loopback addresses",
566 + addr: NetworkAddress{
567 + Network: "tcp",
568 + Host: "localhost",
569 + StartPort: 2019,
570 + EndPort: 2019,
571 + },
572 + origins: nil,
573 + expectOrigins: []string{
574 + "localhost:2019",
575 + "[::1]:2019",
576 + "127.0.0.1:2019",
577 + },
578 + },
579 + }
580 +
581 + for _, test := range tests {
582 + t.Run(test.name, func(t *testing.T) {
583 + admin := AdminConfig{
584 + Origins: test.origins,
585 + }
586 +
587 + got := admin.allowedOrigins(test.addr)
588 +
589 + var gotOrigins []string
590 + for _, u := range got {
591 + gotOrigins = append(gotOrigins, u.Host)
592 + }
593 +
594 + if len(gotOrigins) != len(test.expectOrigins) {
595 + t.Errorf("Expected %d origins but got %d", len(test.expectOrigins), len(gotOrigins))
596 + return
597 + }
598 +
599 + expectMap := make(map[string]struct{})
600 + for _, origin := range test.expectOrigins {
601 + expectMap[origin] = struct{}{}
602 + }
603 +
604 + gotMap := make(map[string]struct{})
605 + for _, origin := range gotOrigins {
606 + gotMap[origin] = struct{}{}
607 + }
608 +
609 + if !reflect.DeepEqual(expectMap, gotMap) {
610 + t.Errorf("Origins mismatch.\nExpected: %v\nGot: %v", test.expectOrigins, gotOrigins)
611 + }
612 + })
613 + }
614 +}
615 +
616 +func TestReplaceRemoteAdminServer(t *testing.T) {
617 + const testCert = `MIIDCTCCAfGgAwIBAgIUXsqJ1mY8pKlHQtI3HJ23x2eZPqwwDQYJKoZIhvcNAQEL
618 +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIzMDEwMTAwMDAwMFoXDTI0MDEw
619 +MTAwMDAwMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
620 +AAOCAQ8AMIIBCgKCAQEA4O4S6BSoYcoxvRqI+h7yPOjF6KjntjzVVm9M+uHK4lzX
621 +F1L3pSxJ2nDD4wZEV3FJ5yFOHVFqkG2vXG3BIczOlYG7UeNmKbQnKc5kZj3HGUrS
622 +VGEktA4OJbeZhhWP15gcXN5eDM2eH3g9BFXVX6AURxLiUXzhNBUEZuj/OEyH9yEF
623 +/qPCE+EjzVvWxvBXwgz/io4r4yok/Vq/bxJ6FlV6R7DX5oJSXyO0VEHZPi9DIyNU
624 +kK3F/r4U1sWiJGWOs8i3YQWZ2ejh1C0aLFZpPcCGGgMNpoF31gyYP6ZuPDUyCXsE
625 +g36UUw1JHNtIXYcLhnXuqj4A8TybTDpgXLqvwA9DBQIDAQABo1MwUTAdBgNVHQ4E
626 +FgQUc13z30pFC63rr/HGKOE7E82vjXwwHwYDVR0jBBgwFoAUc13z30pFC63rr/HG
627 +KOE7E82vjXwwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAHO3j
628 +oeiUXXJ7xD4P8Wj5t9d+E8lE1Xv1Dk3Z+EdG5+dan+RcToE42JJp9zB7FIh5Qz8g
629 +W77LAjqh5oyqz3A2VJcyVgfE3uJP1R1mJM7JfGHf84QH4TZF2Q1RZY4SZs0VQ6+q
630 +5wSlIZ4NXDy4Q4XkIJBGS61wT8IzYFXYBpx4PCP1Qj0PIE4sevEGwjsBIgxK307o
631 +BxF8AWe6N6e4YZmQLGjQ+SeH0iwZb6vpkHyAY8Kj2hvK+cq2P7vU3VGi0t3r1F8L
632 +IvrXHCvO2BMNJ/1UK1M4YNX8LYJqQhg9hEsIROe1OE/m3VhxIYMJI+qZXk9yHfgJ
633 +vq+SH04xKhtFudVBAQ==`
634 +
635 + tests := []struct {
636 + name string
637 + cfg *Config
638 + wantErr bool
639 + }{
640 + {
641 + name: "nil config",
642 + cfg: nil,
643 + wantErr: false,
644 + },
645 + {
646 + name: "nil admin config",
647 + cfg: &Config{
648 + Admin: nil,
649 + },
650 + wantErr: false,
651 + },
652 + {
653 + name: "nil remote config",
654 + cfg: &Config{
655 + Admin: &AdminConfig{},
656 + },
657 + wantErr: false,
658 + },
659 + {
660 + name: "invalid listen address",
661 + cfg: &Config{
662 + Admin: &AdminConfig{
663 + Remote: &RemoteAdmin{
664 + Listen: "invalid:address",
665 + },
666 + },
667 + },
668 + wantErr: true,
669 + },
670 + {
671 + name: "valid config",
672 + cfg: &Config{
673 + Admin: &AdminConfig{
674 + Identity: &IdentityConfig{},
675 + Remote: &RemoteAdmin{
676 + Listen: "localhost:2021",
677 + AccessControl: []*AdminAccess{
678 + {
679 + PublicKeys: []string{testCert},
680 + Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
681 + },
682 + },
683 + },
684 + },
685 + },
686 + wantErr: false,
687 + },
688 + {
689 + name: "invalid certificate",
690 + cfg: &Config{
691 + Admin: &AdminConfig{
692 + Identity: &IdentityConfig{},
693 + Remote: &RemoteAdmin{
694 + Listen: "localhost:2021",
695 + AccessControl: []*AdminAccess{
696 + {
697 + PublicKeys: []string{"invalid-cert-data"},
698 + Permissions: []AdminPermissions{{Methods: []string{"GET"}, Paths: []string{"/test"}}},
699 + },
700 + },
701 + },
702 + },
703 + },
704 + wantErr: true,
705 + },
706 + }
707 +
708 + for _, test := range tests {
709 + t.Run(test.name, func(t *testing.T) {
710 + ctx := Context{
711 + Context: context.Background(),
712 + cfg: test.cfg,
713 + }
714 +
715 + if test.cfg != nil {
716 + test.cfg.storage = &certmagic.FileStorage{Path: t.TempDir()}
717 + }
718 +
719 + if test.cfg != nil && test.cfg.Admin != nil && test.cfg.Admin.Identity != nil {
720 + identityCertCache = certmagic.NewCache(certmagic.CacheOptions{
721 + GetConfigForCert: func(certmagic.Certificate) (*certmagic.Config, error) {
722 + return &certmagic.Config{}, nil
723 + },
724 + })
725 + }
726 +
727 + err := replaceRemoteAdminServer(ctx, test.cfg)
728 +
729 + if test.wantErr {
730 + if err == nil {
731 + t.Error("Expected error but got nil")
732 + }
733 + } else {
734 + if err != nil {
735 + t.Errorf("Expected no error but got: %v", err)
736 + }
737 + }
738 +
739 + // Clean up
740 + if remoteAdminServer != nil {
741 + _ = stopAdminServer(remoteAdminServer)
742 + }
743 + })
744 + }
745 +}
746 +
747 +type mockIssuer struct {
748 + configSet *certmagic.Config
749 +}
750 +
751 +func (m *mockIssuer) Issue(ctx context.Context, csr *x509.CertificateRequest) (*certmagic.IssuedCertificate, error) {
752 + return &certmagic.IssuedCertificate{
753 + Certificate: []byte(csr.Raw),
754 + }, nil
755 +}
756 +
757 +func (m *mockIssuer) SetConfig(cfg *certmagic.Config) {
758 + m.configSet = cfg
759 +}
760 +
761 +func (m *mockIssuer) IssuerKey() string {
762 + return "mock"
763 +}
764 +
765 +type mockIssuerModule struct {
766 + *mockIssuer
767 +}
768 +
769 +func (m *mockIssuerModule) CaddyModule() ModuleInfo {
770 + return ModuleInfo{
771 + ID: "tls.issuance.acme",
772 + New: func() Module {
773 + return &mockIssuerModule{mockIssuer: new(mockIssuer)}
774 + },
775 + }
776 +}
777 +
778 +func TestManageIdentity(t *testing.T) {
779 + originalModules := make(map[string]ModuleInfo)
780 + for k, v := range modules {
781 + originalModules[k] = v
782 + }
783 + defer func() {
784 + modules = originalModules
785 + }()
786 +
787 + RegisterModule(&mockIssuerModule{})
788 +
789 + certPEM := []byte(`-----BEGIN CERTIFICATE-----
790 +MIIDujCCAqKgAwIBAgIIE31FZVaPXTUwDQYJKoZIhvcNAQEFBQAwSTELMAkGA1UE
791 +BhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMTHEdvb2dsZSBJbnRl
792 +cm5ldCBBdXRob3JpdHkgRzIwHhcNMTQwMTI5MTMyNzQzWhcNMTQwNTI5MDAwMDAw
793 +WjBpMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwN
794 +TW91bnRhaW4gVmlldzETMBEGA1UECgwKR29vZ2xlIEluYzEYMBYGA1UEAwwPbWFp
795 +bC5nb29nbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3lcub2pUwkjC
796 +5GJQA2ZZfJJi6d1QHhEmkX9VxKYGp6gagZuRqJWy9TXP6++1ZzQQxqZLD0TkuxZ9
797 +8i9Nz00000CCBjCCAQQwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMGgG
798 +CCsGAQUFBwEBBFwwWjArBggrBgEFBQcwAoYfaHR0cDovL3BraS5nb29nbGUuY29t
799 +L0dJQUcyLmNydDArBggrBgEFBQcwAYYfaHR0cDovL2NsaWVudHMxLmdvb2dsZS5j
800 +b20vb2NzcDAdBgNVHQ4EFgQUiJxtimAuTfwb+aUtBn5UYKreKvMwDAYDVR0TAQH/
801 +BAIwADAfBgNVHSMEGDAWgBRK3QYWG7z2aLV29YG2u2IaulqBLzAXBgNVHREEEDAO
802 +ggxtYWlsLmdvb2dsZTANBgkqhkiG9w0BAQUFAAOCAQEAMP6IWgNGZE8wP9TjFjSZ
803 +3mmW3A1eIr0CuPwNZ2LJ5ZD1i70ojzcj4I9IdP5yPg9CAEV4hNASbM1LzfC7GmJE
804 +tPzW5tRmpKVWZGRgTgZI8Hp/xZXMwLh9ZmXV4kESFAGj5G5FNvJyUV7R5Eh+7OZX
805 +7G4jJ4ZGJh+5jzN9HdJJHQHGYNIYOzC7+HH9UMwCjX9vhQ4RjwFZJThS2Yb+y7pb
806 +9yxTJZoXC6J0H5JpnZb7kZEJ+Xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
807 +-----END CERTIFICATE-----`)
808 +
809 + keyPEM := []byte(`-----BEGIN PRIVATE KEY-----
810 +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRS0LmTwUT0iwP
811 +...
812 +-----END PRIVATE KEY-----`)
813 +
814 + testStorage := certmagic.FileStorage{Path: t.TempDir()}
815 + err := testStorage.Store(context.Background(), "localhost/localhost.crt", certPEM)
816 + if err != nil {
817 + t.Fatal(err)
818 + }
819 + err = testStorage.Store(context.Background(), "localhost/localhost.key", keyPEM)
820 + if err != nil {
821 + t.Fatal(err)
822 + }
823 +
824 + tests := []struct {
825 + name string
826 + cfg *Config
827 + wantErr bool
828 + checkState func(*testing.T, *Config)
829 + }{
830 + {
831 + name: "nil config",
832 + cfg: nil,
833 + },
834 + {
835 + name: "nil admin config",
836 + cfg: &Config{
837 + Admin: nil,
838 + },
839 + },
840 + {
841 + name: "nil identity config",
842 + cfg: &Config{
843 + Admin: &AdminConfig{},
844 + },
845 + },
846 + {
847 + name: "default issuer when none specified",
848 + cfg: &Config{
849 + Admin: &AdminConfig{
850 + Identity: &IdentityConfig{
851 + Identifiers: []string{"localhost"},
852 + },
853 + },
854 + storage: &testStorage,
855 + },
856 + checkState: func(t *testing.T, cfg *Config) {
857 + if len(cfg.Admin.Identity.issuers) == 0 {
858 + t.Error("Expected at least 1 issuer to be configured")
859 + return
860 + }
861 + if _, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule); !ok {
862 + t.Error("Expected mock issuer to be configured")
863 + }
864 + },
865 + },
866 + {
867 + name: "custom issuer",
868 + cfg: &Config{
869 + Admin: &AdminConfig{
870 + Identity: &IdentityConfig{
871 + Identifiers: []string{"localhost"},
872 + IssuersRaw: []json.RawMessage{
873 + json.RawMessage(`{"module": "acme"}`),
874 + },
875 + },
876 + },
877 + storage: &certmagic.FileStorage{Path: "testdata"},
878 + },
879 + checkState: func(t *testing.T, cfg *Config) {
880 + if len(cfg.Admin.Identity.issuers) != 1 {
881 + t.Fatalf("Expected 1 issuer, got %d", len(cfg.Admin.Identity.issuers))
882 + }
883 + mockIss, ok := cfg.Admin.Identity.issuers[0].(*mockIssuerModule)
884 + if !ok {
885 + t.Fatal("Expected mock issuer")
886 + }
887 + if mockIss.configSet == nil {
888 + t.Error("Issuer config was not set")
889 + }
890 + },
891 + },
892 + {
893 + name: "invalid issuer module",
894 + cfg: &Config{
895 + Admin: &AdminConfig{
896 + Identity: &IdentityConfig{
897 + Identifiers: []string{"localhost"},
898 + IssuersRaw: []json.RawMessage{
899 + json.RawMessage(`{"module": "doesnt_exist"}`),
900 + },
901 + },
902 + },
903 + },
904 + wantErr: true,
905 + },
906 + }
907 +
908 + for _, test := range tests {
909 + t.Run(test.name, func(t *testing.T) {
910 + if identityCertCache != nil {
911 + // Reset the cert cache before each test
912 + identityCertCache.Stop()
913 + identityCertCache = nil
914 + }
915 +
916 + ctx := Context{
917 + Context: context.Background(),
918 + cfg: test.cfg,
919 + moduleInstances: make(map[string][]Module),
920 + }
921 +
922 + err := manageIdentity(ctx, test.cfg)
923 +
924 + if test.wantErr {
925 + if err == nil {
926 + t.Error("Expected error but got nil")
927 + }
928 + return
929 + }
930 + if err != nil {
931 + t.Fatalf("Expected no error but got: %v", err)
932 + }
933 +
934 + if test.checkState != nil {
935 + test.checkState(t, test.cfg)
936 + }
937 + })
938 + }
939 +}