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(`{
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
+}