mirror of
https://github.com/netbirdio/netbird-crossplane-provider.git
synced 2026-05-22 18:44:14 -07:00
251 lines
8.0 KiB
Go
251 lines
8.0 KiB
Go
/*
|
|
Copyright 2022 The Crossplane Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package nbnetwork
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/crossplane/crossplane-runtime/pkg/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"github.com/crossplane/netbird-crossplane-provider/apis/vpn/v1alpha1"
|
|
|
|
netbird "github.com/netbirdio/netbird/management/client/rest"
|
|
)
|
|
|
|
// Unlike many Kubernetes projects Crossplane does not use third party testing
|
|
// libraries, per the common Go test review comments. Crossplane encourages the
|
|
// use of table driven unit tests. The tests of the crossplane-runtime project
|
|
// are representative of the testing style Crossplane encourages.
|
|
//
|
|
// https://github.com/golang/go/wiki/TestComments
|
|
// https://github.com/crossplane/crossplane/blob/master/CONTRIBUTING.md#contributing-code
|
|
|
|
// fakeAuthClient satisfies the authClient interface and returns a real netbird
|
|
// client pointed at an httptest server.
|
|
type fakeAuthClient struct {
|
|
client *netbird.Client
|
|
}
|
|
|
|
// GetClient returns the stubbed netbird REST client.
|
|
func (f *fakeAuthClient) GetClient(_ context.Context) (*netbird.Client, error) {
|
|
return f.client, nil
|
|
}
|
|
|
|
// ForceRefresh is a no-op in the fake.
|
|
func (f *fakeAuthClient) ForceRefresh(_ context.Context) error { return nil }
|
|
|
|
// newFakeAuth returns a fakeAuthClient backed by an httptest server.
|
|
func newFakeAuth(t *testing.T, h http.HandlerFunc) (*fakeAuthClient, *httptest.Server) {
|
|
t.Helper()
|
|
srv := httptest.NewServer(h)
|
|
c := netbird.NewWithOptions(
|
|
netbird.WithManagementURL(srv.URL),
|
|
netbird.WithPAT("test-token"),
|
|
)
|
|
return &fakeAuthClient{client: c}, srv
|
|
}
|
|
|
|
// TestResolveNetworkLookupID exercises the lookup-ID resolver helper.
|
|
func TestResolveNetworkLookupID(t *testing.T) {
|
|
cases := map[string]struct {
|
|
cr *v1alpha1.NbNetwork
|
|
want string
|
|
}{
|
|
"EmptyExternalNameWithStatusID": {
|
|
cr: func() *v1alpha1.NbNetwork {
|
|
n := &v1alpha1.NbNetwork{}
|
|
n.Status.AtProvider.Id = "real-id"
|
|
return n
|
|
}(),
|
|
want: "real-id",
|
|
},
|
|
"EmptyExternalNameAndEmptyStatus": {
|
|
cr: &v1alpha1.NbNetwork{},
|
|
want: "",
|
|
},
|
|
"ExternalNameDefaultedToObjectNameRecoversFromStatus": {
|
|
cr: func() *v1alpha1.NbNetwork {
|
|
n := &v1alpha1.NbNetwork{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "my-net"},
|
|
}
|
|
meta.SetExternalName(n, "my-net")
|
|
n.Status.AtProvider.Id = "real-id"
|
|
return n
|
|
}(),
|
|
want: "real-id",
|
|
},
|
|
"ExternalNameSetToRealID": {
|
|
cr: func() *v1alpha1.NbNetwork {
|
|
n := &v1alpha1.NbNetwork{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "my-net"},
|
|
}
|
|
meta.SetExternalName(n, "real-id")
|
|
return n
|
|
}(),
|
|
want: "real-id",
|
|
},
|
|
}
|
|
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
if got := resolveNetworkLookupID(tc.cr); got != tc.want {
|
|
t.Errorf("resolveNetworkLookupID() = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIsNetworkNotFoundError exercises the not-found error matcher.
|
|
func TestIsNetworkNotFoundError(t *testing.T) {
|
|
cases := map[string]struct {
|
|
err error
|
|
want bool
|
|
}{
|
|
"NilError": {err: nil, want: false},
|
|
"UnrelatedError": {err: errors.New("kaboom"), want: false},
|
|
"NetworkNotFound": {err: errors.New("network: abc not found"), want: true},
|
|
"CaseInsensitive": {err: errors.New("Network NOT FOUND"), want: true},
|
|
"OnlyContainsNetwork": {err: errors.New("network: invalid request"), want: false},
|
|
"OnlyContainsNotFound": {
|
|
err: errors.New("group: abc not found"), want: false,
|
|
},
|
|
}
|
|
for name, tc := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
if got := isNetworkNotFoundError(tc.err); got != tc.want {
|
|
t.Errorf("isNetworkNotFoundError(%v) = %v, want %v", tc.err, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestObserve covers the Observe code paths in this controller.
|
|
func TestObserve(t *testing.T) {
|
|
t.Run("EmptyExternalNameAndEmptyStatusReturnsResourceExistsFalse", func(t *testing.T) {
|
|
// No HTTP traffic should occur because the by-name adoption path lists
|
|
// networks and finds nothing matching the spec name.
|
|
auth, srv := newFakeAuth(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/networks" && r.Method == http.MethodGet {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`[]`))
|
|
return
|
|
}
|
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
})
|
|
defer srv.Close()
|
|
|
|
e := external{authManager: auth}
|
|
cr := &v1alpha1.NbNetwork{
|
|
ObjectMeta: metav1.ObjectMeta{Name: "my-net"},
|
|
}
|
|
cr.Spec.ForProvider.Name = "my-net"
|
|
|
|
obs, err := e.Observe(context.Background(), cr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if obs.ResourceExists {
|
|
t.Errorf("expected ResourceExists=false, got %+v", obs)
|
|
}
|
|
})
|
|
|
|
t.Run("ByIDNotFoundReturnsResourceExistsFalse", func(t *testing.T) {
|
|
auth, srv := newFakeAuth(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/networks/real-id" && r.Method == http.MethodGet {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Write([]byte(`{"message":"network: real-id not found","code":404}`))
|
|
return
|
|
}
|
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
})
|
|
defer srv.Close()
|
|
|
|
e := external{authManager: auth}
|
|
cr := &v1alpha1.NbNetwork{ObjectMeta: metav1.ObjectMeta{Name: "k8s-name"}}
|
|
meta.SetExternalName(cr, "real-id")
|
|
|
|
obs, err := e.Observe(context.Background(), cr)
|
|
if err != nil {
|
|
t.Fatalf("expected no error for not-found, got %v", err)
|
|
}
|
|
if obs.ResourceExists {
|
|
t.Errorf("expected ResourceExists=false on not-found, got %+v", obs)
|
|
}
|
|
})
|
|
|
|
t.Run("ByIDTransientErrorReturnsErrorNotResourceExistsFalse", func(t *testing.T) {
|
|
auth, srv := newFakeAuth(t, func(w http.ResponseWriter, r *http.Request) {
|
|
// Return a 503 with a body that does NOT look like a not-found.
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
w.Write([]byte(`{"message":"upstream unavailable","code":503}`))
|
|
})
|
|
defer srv.Close()
|
|
|
|
e := external{authManager: auth}
|
|
cr := &v1alpha1.NbNetwork{ObjectMeta: metav1.ObjectMeta{Name: "k8s-name"}}
|
|
meta.SetExternalName(cr, "real-id")
|
|
|
|
obs, err := e.Observe(context.Background(), cr)
|
|
if err == nil {
|
|
t.Fatalf("expected error on transient failure, got nil (obs=%+v)", obs)
|
|
}
|
|
if obs.ResourceExists {
|
|
t.Errorf("expected ResourceExists=false on transient error (Create must not run), got %+v", obs)
|
|
}
|
|
if !strings.Contains(err.Error(), "failed to observe network") {
|
|
t.Errorf("expected wrapped error mentioning network, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("StaleExternalNameRepairedFromStatusID", func(t *testing.T) {
|
|
auth, srv := newFakeAuth(t, func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/api/networks/real-id" && r.Method == http.MethodGet {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"id":"real-id","name":"my-net","description":"d"}`))
|
|
return
|
|
}
|
|
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
})
|
|
defer srv.Close()
|
|
|
|
e := external{authManager: auth}
|
|
cr := &v1alpha1.NbNetwork{ObjectMeta: metav1.ObjectMeta{Name: "my-net"}}
|
|
// Simulate the older defaulted external-name scenario: external-name == k8s name.
|
|
meta.SetExternalName(cr, "my-net")
|
|
cr.Status.AtProvider.Id = "real-id"
|
|
cr.Spec.ForProvider.Name = "my-net"
|
|
cr.Spec.ForProvider.Description = "d"
|
|
|
|
obs, err := e.Observe(context.Background(), cr)
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if !obs.ResourceExists {
|
|
t.Errorf("expected ResourceExists=true after recovery, got %+v", obs)
|
|
}
|
|
if got := meta.GetExternalName(cr); got != "real-id" {
|
|
t.Errorf("expected external-name repaired to 'real-id', got %q", got)
|
|
}
|
|
})
|
|
}
|