@@ -511,3 +511,146 @@ var _ = Describe("RedfishBaseBMC findExistingSubscription", func() {
511511 Expect (link ).To (BeEmpty ())
512512 })
513513})
514+
515+ var _ = Describe ("RedfishBaseBMC CreateEventSubscription" , func () {
516+ ctx := context .Background ()
517+ const destination = "http://operator/serverevents/alerts/node1"
518+
519+ // baseHandlers registers the service root and event service on mux.
520+ baseHandlers := func (mux * http.ServeMux ) {
521+ mux .HandleFunc ("/redfish/v1/" , func (w http.ResponseWriter , _ * http.Request ) {
522+ w .Header ().Set ("Content-Type" , "application/json" )
523+ w .Write (serviceRootWithEventServiceJSON ()) //nolint:errcheck
524+ })
525+ mux .HandleFunc ("/redfish/v1/EventService" , func (w http.ResponseWriter , _ * http.Request ) {
526+ w .Header ().Set ("Content-Type" , "application/json" )
527+ w .Write (eventServiceJSON ()) //nolint:errcheck
528+ })
529+ }
530+
531+ It ("should return the subscription link from Location header on success" , func () {
532+ mux := http .NewServeMux ()
533+ baseHandlers (mux )
534+ mux .HandleFunc ("/redfish/v1/EventService/Subscriptions" , func (w http.ResponseWriter , r * http.Request ) {
535+ if r .Method == http .MethodPost {
536+ w .Header ().Set ("Location" , "/redfish/v1/EventService/Subscriptions/new-1" )
537+ w .WriteHeader (http .StatusCreated )
538+ return
539+ }
540+ w .Header ().Set ("Content-Type" , "application/json" )
541+ w .Write (subscriptionsCollectionJSON ([]string {})) //nolint:errcheck
542+ })
543+
544+ server := httptest .NewServer (mux )
545+ defer server .Close ()
546+
547+ bmc := newTestRedfishBMC (server )
548+ link , err := bmc .CreateEventSubscription (ctx , destination , schemas .EventFormatType ("Event" ), schemas .DeliveryRetryPolicy ("RetryForever" ))
549+ Expect (err ).NotTo (HaveOccurred ())
550+ Expect (link ).To (Equal ("/redfish/v1/EventService/Subscriptions/new-1" ))
551+ })
552+
553+ // This test is the regression guard for the fix. Gofish returns non-2xx responses as errors —
554+ // the subscription link can never be recovered from resp.StatusCode because resp is nil when
555+ // err != nil. The recovery MUST happen via err.Error() in the if-err block.
556+ // If someone re-introduces a resp.StatusCode check instead, this test will fail because
557+ // findExistingSubscription will never be called and the function will return the raw error.
558+ It ("should recover existing subscription when BMC returns ResourceAlreadyExists (via err.Error)" , func () {
559+ mux := http .NewServeMux ()
560+ baseHandlers (mux )
561+ mux .HandleFunc ("/redfish/v1/EventService/Subscriptions" , func (w http.ResponseWriter , r * http.Request ) {
562+ if r .Method == http .MethodPost {
563+ // Gofish wraps this 409 as an error containing the body text.
564+ // strings.Contains(err.Error(), "ResourceAlreadyExists") must be true.
565+ w .WriteHeader (http .StatusConflict )
566+ w .Write ([]byte (`{"error":{"@Message.ExtendedInfo":[{"MessageId":"Base.1.0.ResourceAlreadyExists","Message":"already exists"}]}}` )) //nolint:errcheck
567+ return
568+ }
569+ w .Header ().Set ("Content-Type" , "application/json" )
570+ w .Write (subscriptionsCollectionJSON ([]string {"/redfish/v1/EventService/Subscriptions/existing-1" })) //nolint:errcheck
571+ })
572+ mux .HandleFunc ("/redfish/v1/EventService/Subscriptions/existing-1" , func (w http.ResponseWriter , _ * http.Request ) {
573+ w .Header ().Set ("Content-Type" , "application/json" )
574+ w .Write (subscriptionJSON ("existing-1" , destination , "metal-operator" )) //nolint:errcheck
575+ })
576+
577+ server := httptest .NewServer (mux )
578+ defer server .Close ()
579+
580+ bmc := newTestRedfishBMC (server )
581+ link , err := bmc .CreateEventSubscription (ctx , destination , schemas .EventFormatType ("Event" ), schemas .DeliveryRetryPolicy ("RetryForever" ))
582+ Expect (err ).NotTo (HaveOccurred ())
583+ Expect (link ).To (Equal ("/redfish/v1/EventService/Subscriptions/existing-1" ))
584+ })
585+
586+ It ("should recover existing subscription when BMC returns PropertyValueModified (via err.Error)" , func () {
587+ mux := http .NewServeMux ()
588+ baseHandlers (mux )
589+ mux .HandleFunc ("/redfish/v1/EventService/Subscriptions" , func (w http.ResponseWriter , r * http.Request ) {
590+ if r .Method == http .MethodPost {
591+ w .WriteHeader (http .StatusBadRequest )
592+ w .Write ([]byte (`{"error":{"@Message.ExtendedInfo":[{"MessageId":"Base.1.0.PropertyValueModified","Message":"already exists"}]}}` )) //nolint:errcheck
593+ return
594+ }
595+ w .Header ().Set ("Content-Type" , "application/json" )
596+ w .Write (subscriptionsCollectionJSON ([]string {"/redfish/v1/EventService/Subscriptions/existing-1" })) //nolint:errcheck
597+ })
598+ mux .HandleFunc ("/redfish/v1/EventService/Subscriptions/existing-1" , func (w http.ResponseWriter , _ * http.Request ) {
599+ w .Header ().Set ("Content-Type" , "application/json" )
600+ w .Write (subscriptionJSON ("existing-1" , destination , "metal-operator" )) //nolint:errcheck
601+ })
602+
603+ server := httptest .NewServer (mux )
604+ defer server .Close ()
605+
606+ bmc := newTestRedfishBMC (server )
607+ link , err := bmc .CreateEventSubscription (ctx , destination , schemas .EventFormatType ("Event" ), schemas .DeliveryRetryPolicy ("RetryForever" ))
608+ Expect (err ).NotTo (HaveOccurred ())
609+ Expect (link ).To (Equal ("/redfish/v1/EventService/Subscriptions/existing-1" ))
610+ })
611+
612+ It ("should return error when BMC returns ResourceAlreadyExists but existing subscription cannot be found" , func () {
613+ mux := http .NewServeMux ()
614+ baseHandlers (mux )
615+ mux .HandleFunc ("/redfish/v1/EventService/Subscriptions" , func (w http.ResponseWriter , r * http.Request ) {
616+ if r .Method == http .MethodPost {
617+ w .WriteHeader (http .StatusConflict )
618+ w .Write ([]byte (`{"error":{"@Message.ExtendedInfo":[{"MessageId":"Base.1.0.ResourceAlreadyExists"}]}}` )) //nolint:errcheck
619+ return
620+ }
621+ // Return empty collection so findExistingSubscription finds nothing.
622+ w .Header ().Set ("Content-Type" , "application/json" )
623+ w .Write (subscriptionsCollectionJSON ([]string {})) //nolint:errcheck
624+ })
625+
626+ server := httptest .NewServer (mux )
627+ defer server .Close ()
628+
629+ bmc := newTestRedfishBMC (server )
630+ link , err := bmc .CreateEventSubscription (ctx , destination , schemas .EventFormatType ("Event" ), schemas .DeliveryRetryPolicy ("RetryForever" ))
631+ Expect (err ).To (HaveOccurred ())
632+ Expect (err .Error ()).To (ContainSubstring ("failed to recover existing subscription" ))
633+ Expect (link ).To (BeEmpty ())
634+ })
635+
636+ It ("should propagate non-duplicate POST errors unchanged" , func () {
637+ mux := http .NewServeMux ()
638+ baseHandlers (mux )
639+ mux .HandleFunc ("/redfish/v1/EventService/Subscriptions" , func (w http.ResponseWriter , r * http.Request ) {
640+ if r .Method == http .MethodPost {
641+ w .WriteHeader (http .StatusInternalServerError )
642+ w .Write ([]byte (`{"error":{"message":"internal server error"}}` )) //nolint:errcheck
643+ return
644+ }
645+ })
646+
647+ server := httptest .NewServer (mux )
648+ defer server .Close ()
649+
650+ bmc := newTestRedfishBMC (server )
651+ link , err := bmc .CreateEventSubscription (ctx , destination , schemas .EventFormatType ("Event" ), schemas .DeliveryRetryPolicy ("RetryForever" ))
652+ Expect (err ).To (HaveOccurred ())
653+ Expect (err .Error ()).NotTo (ContainSubstring ("failed to recover existing subscription" ))
654+ Expect (link ).To (BeEmpty ())
655+ })
656+ })
0 commit comments