diff --git a/management/client/rest/accounts.go b/management/client/rest/accounts.go new file mode 100644 index 00000000000..f38b19f7061 --- /dev/null +++ b/management/client/rest/accounts.go @@ -0,0 +1,54 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// AccountsAPI APIs for accounts, do not use directly +type AccountsAPI struct { + c *Client +} + +// List list all accounts, only returns one account always +// See more: https://docs.netbird.io/api/resources/accounts#list-all-accounts +func (a *AccountsAPI) List(ctx context.Context) ([]api.Account, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/accounts", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Account](resp) + return ret, err +} + +// Update update account settings +// See more: https://docs.netbird.io/api/resources/accounts#update-an-account +func (a *AccountsAPI) Update(ctx context.Context, accountID string, request api.PutApiAccountsAccountIdJSONRequestBody) (*api.Account, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/accounts/"+accountID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Account](resp) + return &ret, err +} + +// Delete delete account +// See more: https://docs.netbird.io/api/resources/accounts#delete-an-account +func (a *AccountsAPI) Delete(ctx context.Context, accountID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/accounts/"+accountID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/accounts_test.go b/management/client/rest/accounts_test.go new file mode 100644 index 00000000000..3c1925fbcb3 --- /dev/null +++ b/management/client/rest/accounts_test.go @@ -0,0 +1,169 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testAccount = api.Account{ + Id: "Test", + Settings: api.AccountSettings{ + Extra: &api.AccountExtraSettings{ + PeerApprovalEnabled: ptr(false), + }, + GroupsPropagationEnabled: ptr(true), + JwtGroupsEnabled: ptr(false), + PeerInactivityExpiration: 7, + PeerInactivityExpirationEnabled: true, + PeerLoginExpiration: 24, + PeerLoginExpirationEnabled: true, + RegularUsersViewBlocked: false, + RoutingPeerDnsResolutionEnabled: ptr(false), + }, + } +) + +func TestAccounts_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Account{testAccount}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testAccount, ret[0]) + }) +} + +func TestAccounts_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestAccounts_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiAccountsAccountIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, *req.Settings.RoutingPeerDnsResolutionEnabled) + retBytes, _ := json.Marshal(testAccount) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{ + Settings: api.AccountSettings{ + RoutingPeerDnsResolutionEnabled: ptr(true), + }, + }) + require.NoError(t, err) + assert.Equal(t, testAccount, *ret) + }) + +} + +func TestAccounts_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Accounts.Update(context.Background(), "Test", api.PutApiAccountsAccountIdJSONRequestBody{ + Settings: api.AccountSettings{ + RoutingPeerDnsResolutionEnabled: ptr(true), + }, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestAccounts_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Accounts.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestAccounts_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/accounts/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Accounts.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestAccounts_Integration_List(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + accounts, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, accounts, 1) + assert.Equal(t, "bf1c8084-ba50-4ce7-9439-34653001fc3b", accounts[0].Id) + assert.Equal(t, false, *accounts[0].Settings.Extra.PeerApprovalEnabled) + }) +} + +func TestAccounts_Integration_Update(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + accounts, err := c.Accounts.List(context.Background()) + require.NoError(t, err) + assert.Len(t, accounts, 1) + accounts[0].Settings.JwtAllowGroups = ptr([]string{"test"}) + account, err := c.Accounts.Update(context.Background(), accounts[0].Id, api.AccountRequest{ + Settings: accounts[0].Settings, + }) + require.NoError(t, err) + assert.Equal(t, accounts[0].Id, account.Id) + assert.Equal(t, []string{"test"}, *account.Settings.JwtAllowGroups) + }) +} + +// Account deletion on MySQL and PostgreSQL databases causes unknown errors +// func TestAccounts_Integration_Delete(t *testing.T) { +// withBlackBoxServer(t, func(c *Client) { +// accounts, err := c.Accounts.List(context.Background()) +// require.NoError(t, err) +// assert.Len(t, accounts, 1) +// err = c.Accounts.Delete(context.Background(), accounts[0].Id) +// require.NoError(t, err) +// _, err = c.Accounts.List(context.Background()) +// assert.Error(t, err) +// }) +// } diff --git a/management/client/rest/client.go b/management/client/rest/client.go new file mode 100644 index 00000000000..f55e2d11e6a --- /dev/null +++ b/management/client/rest/client.go @@ -0,0 +1,133 @@ +package rest + +import ( + "context" + "encoding/json" + "errors" + "io" + "net/http" + + "github.com/netbirdio/netbird/management/server/http/util" +) + +// Client Management service HTTP REST API Client +type Client struct { + managementURL string + authHeader string + + // Accounts NetBird account APIs + // see more: https://docs.netbird.io/api/resources/accounts + Accounts *AccountsAPI + + // Users NetBird users APIs + // see more: https://docs.netbird.io/api/resources/users + Users *UsersAPI + + // Tokens NetBird tokens APIs + // see more: https://docs.netbird.io/api/resources/tokens + Tokens *TokensAPI + + // Peers NetBird peers APIs + // see more: https://docs.netbird.io/api/resources/peers + Peers *PeersAPI + + // SetupKeys NetBird setup keys APIs + // see more: https://docs.netbird.io/api/resources/setup-keys + SetupKeys *SetupKeysAPI + + // Groups NetBird groups APIs + // see more: https://docs.netbird.io/api/resources/groups + Groups *GroupsAPI + + // Policies NetBird policies APIs + // see more: https://docs.netbird.io/api/resources/policies + Policies *PoliciesAPI + + // PostureChecks NetBird posture checks APIs + // see more: https://docs.netbird.io/api/resources/posture-checks + PostureChecks *PostureChecksAPI + + // Networks NetBird networks APIs + // see more: https://docs.netbird.io/api/resources/networks + Networks *NetworksAPI + + // Routes NetBird routes APIs + // see more: https://docs.netbird.io/api/resources/routes + Routes *RoutesAPI + + // DNS NetBird DNS APIs + // see more: https://docs.netbird.io/api/resources/routes + DNS *DNSAPI + + // GeoLocation NetBird Geo Location APIs + // see more: https://docs.netbird.io/api/resources/geo-locations + GeoLocation *GeoLocationAPI + + // Events NetBird Events APIs + // see more: https://docs.netbird.io/api/resources/events + Events *EventsAPI +} + +// New initialize new Client instance +func New(managementURL, token string) *Client { + client := &Client{ + managementURL: managementURL, + authHeader: "Token " + token, + } + client.Accounts = &AccountsAPI{client} + client.Users = &UsersAPI{client} + client.Tokens = &TokensAPI{client} + client.Peers = &PeersAPI{client} + client.SetupKeys = &SetupKeysAPI{client} + client.Groups = &GroupsAPI{client} + client.Policies = &PoliciesAPI{client} + client.PostureChecks = &PostureChecksAPI{client} + client.Networks = &NetworksAPI{client} + client.Routes = &RoutesAPI{client} + client.DNS = &DNSAPI{client} + client.GeoLocation = &GeoLocationAPI{client} + client.Events = &EventsAPI{client} + return client +} + +func (c *Client) newRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, method, c.managementURL+path, body) + if err != nil { + return nil, err + } + + req.Header.Add("Authorization", c.authHeader) + req.Header.Add("Accept", "application/json") + if body != nil { + req.Header.Add("Content-Type", "application/json") + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode > 299 { + parsedErr, pErr := parseResponse[util.ErrorResponse](resp) + if pErr != nil { + return nil, err + } + return nil, errors.New(parsedErr.Message) + } + + return resp, nil +} + +func parseResponse[T any](resp *http.Response) (T, error) { + var ret T + if resp.Body == nil { + return ret, errors.New("No body") + } + bs, err := io.ReadAll(resp.Body) + if err != nil { + return ret, err + } + err = json.Unmarshal(bs, &ret) + + return ret, err +} diff --git a/management/client/rest/client_test.go b/management/client/rest/client_test.go new file mode 100644 index 00000000000..a42b12fa326 --- /dev/null +++ b/management/client/rest/client_test.go @@ -0,0 +1,30 @@ +package rest + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/netbirdio/netbird/management/server/http/testing/testing_tools" +) + +func withMockClient(callback func(*Client, *http.ServeMux)) { + mux := &http.ServeMux{} + server := httptest.NewServer(mux) + defer server.Close() + c := New(server.URL, "ABC") + callback(c, mux) +} + +func ptr[T any, PT *T](x T) PT { + return &x +} + +func withBlackBoxServer(t *testing.T, callback func(*Client)) { + t.Helper() + handler, _, _ := testing_tools.BuildApiBlackBoxWithDBState(t, "../../server/testdata/store.sql", nil, false) + server := httptest.NewServer(handler) + defer server.Close() + c := New(server.URL, "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp") + callback(c) +} diff --git a/management/client/rest/dns.go b/management/client/rest/dns.go new file mode 100644 index 00000000000..ef9923b1f8e --- /dev/null +++ b/management/client/rest/dns.go @@ -0,0 +1,110 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// DNSAPI APIs for DNS Management, do not use directly +type DNSAPI struct { + c *Client +} + +// ListNameserverGroups list all nameserver groups +// See more: https://docs.netbird.io/api/resources/dns#list-all-nameserver-groups +func (a *DNSAPI) ListNameserverGroups(ctx context.Context) ([]api.NameserverGroup, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NameserverGroup](resp) + return ret, err +} + +// GetNameserverGroup get nameserver group info +// See more: https://docs.netbird.io/api/resources/dns#retrieve-a-nameserver-group +func (a *DNSAPI) GetNameserverGroup(ctx context.Context, nameserverGroupID string) (*api.NameserverGroup, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/nameservers/"+nameserverGroupID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// CreateNameserverGroup create new nameserver group +// See more: https://docs.netbird.io/api/resources/dns#create-a-nameserver-group +func (a *DNSAPI) CreateNameserverGroup(ctx context.Context, request api.PostApiDnsNameserversJSONRequestBody) (*api.NameserverGroup, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/dns/nameservers", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// UpdateNameserverGroup update nameserver group info +// See more: https://docs.netbird.io/api/resources/dns#update-a-nameserver-group +func (a *DNSAPI) UpdateNameserverGroup(ctx context.Context, nameserverGroupID string, request api.PutApiDnsNameserversNsgroupIdJSONRequestBody) (*api.NameserverGroup, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/nameservers/"+nameserverGroupID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NameserverGroup](resp) + return &ret, err +} + +// DeleteNameserverGroup delete nameserver group +// See more: https://docs.netbird.io/api/resources/dns#delete-a-nameserver-group +func (a *DNSAPI) DeleteNameserverGroup(ctx context.Context, nameserverGroupID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/dns/nameservers/"+nameserverGroupID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// GetSettings get DNS settings +// See more: https://docs.netbird.io/api/resources/dns#retrieve-dns-settings +func (a *DNSAPI) GetSettings(ctx context.Context) (*api.DNSSettings, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/dns/settings", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.DNSSettings](resp) + return &ret, err +} + +// UpdateSettings update DNS settings +// See more: https://docs.netbird.io/api/resources/dns#update-dns-settings +func (a *DNSAPI) UpdateSettings(ctx context.Context, request api.PutApiDnsSettingsJSONRequestBody) (*api.DNSSettings, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/dns/settings", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.DNSSettings](resp) + return &ret, err +} diff --git a/management/client/rest/dns_test.go b/management/client/rest/dns_test.go new file mode 100644 index 00000000000..d2c00549c1d --- /dev/null +++ b/management/client/rest/dns_test.go @@ -0,0 +1,295 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testNameserverGroup = api.NameserverGroup{ + Id: "Test", + Name: "wow", + } + + testSettings = api.DNSSettings{ + DisabledManagementGroups: []string{"gone"}, + } +) + +func TestDNSNameserverGroup_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NameserverGroup{testNameserverGroup}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNameserverGroup, ret[0]) + }) +} + +func TestDNSNameserverGroup_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.ListNameserverGroups(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSNameserverGroup_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNameserverGroup) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetNameserverGroup(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetNameserverGroup(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSNameserverGroup_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiDnsNameserversJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNameserverGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.CreateNameserverGroup(context.Background(), api.PostApiDnsNameserversJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.CreateNameserverGroup(context.Background(), api.PostApiDnsNameserversJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNSNameserverGroup_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiDnsNameserversNsgroupIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNameserverGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateNameserverGroup(context.Background(), "Test", api.PutApiDnsNameserversNsgroupIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNameserverGroup, *ret) + }) +} + +func TestDNSNameserverGroup_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateNameserverGroup(context.Background(), "Test", api.PutApiDnsNameserversNsgroupIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNSNameserverGroup_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.DNS.DeleteNameserverGroup(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestDNSNameserverGroup_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/nameservers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.DNS.DeleteNameserverGroup(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestDNSSettings_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testSettings) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetSettings(context.Background()) + require.NoError(t, err) + assert.Equal(t, testSettings, *ret) + }) +} + +func TestDNSSettings_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.GetSettings(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestDNSSettings_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiDnsSettingsJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, []string{"test"}, req.DisabledManagementGroups) + retBytes, _ := json.Marshal(testSettings) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateSettings(context.Background(), api.PutApiDnsSettingsJSONRequestBody{ + DisabledManagementGroups: []string{"test"}, + }) + require.NoError(t, err) + assert.Equal(t, testSettings, *ret) + }) +} + +func TestDNSSettings_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/dns/settings", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.DNS.UpdateSettings(context.Background(), api.PutApiDnsSettingsJSONRequestBody{ + DisabledManagementGroups: []string{"test"}, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestDNS_Integration(t *testing.T) { + nsGroupReq := api.NameserverGroupRequest{ + Description: "Test", + Enabled: true, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + Name: "test", + Nameservers: []api.Nameserver{ + { + Ip: "8.8.8.8", + NsType: api.NameserverNsTypeUdp, + Port: 53, + }, + }, + Primary: true, + SearchDomainsEnabled: false, + } + withBlackBoxServer(t, func(c *Client) { + // Create + nsGroup, err := c.DNS.CreateNameserverGroup(context.Background(), nsGroupReq) + require.NoError(t, err) + + // List + nsGroups, err := c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Equal(t, *nsGroup, nsGroups[0]) + + // Update + nsGroupReq.Description = "TestUpdate" + nsGroup, err = c.DNS.UpdateNameserverGroup(context.Background(), nsGroup.Id, nsGroupReq) + require.NoError(t, err) + assert.Equal(t, "TestUpdate", nsGroup.Description) + + // Delete + err = c.DNS.DeleteNameserverGroup(context.Background(), nsGroup.Id) + require.NoError(t, err) + + // List again to ensure deletion + nsGroups, err = c.DNS.ListNameserverGroups(context.Background()) + require.NoError(t, err) + assert.Len(t, nsGroups, 0) + }) +} diff --git a/management/client/rest/events.go b/management/client/rest/events.go new file mode 100644 index 00000000000..1157700ff21 --- /dev/null +++ b/management/client/rest/events.go @@ -0,0 +1,24 @@ +package rest + +import ( + "context" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// EventsAPI APIs for Events, do not use directly +type EventsAPI struct { + c *Client +} + +// List list all events +// See more: https://docs.netbird.io/api/resources/events#list-all-events +func (a *EventsAPI) List(ctx context.Context) ([]api.Event, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/events", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Event](resp) + return ret, err +} diff --git a/management/client/rest/events_test.go b/management/client/rest/events_test.go new file mode 100644 index 00000000000..515c227e6ef --- /dev/null +++ b/management/client/rest/events_test.go @@ -0,0 +1,65 @@ +package rest + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testEvent = api.Event{ + Activity: "AccountCreate", + ActivityCode: api.EventActivityCodeAccountCreate, + } +) + +func TestEvents_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Event{testEvent}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Events.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testEvent, ret[0]) + }) +} + +func TestEvents_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/events", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Events.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestEvents_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + // Do something that would trigger any event + _, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{ + Ephemeral: ptr(true), + Name: "TestSetupKey", + Type: "reusable", + }) + require.NoError(t, err) + + events, err := c.Events.List(context.Background()) + require.NoError(t, err) + assert.NotEmpty(t, events) + }) +} diff --git a/management/client/rest/geo.go b/management/client/rest/geo.go new file mode 100644 index 00000000000..ed9090fe211 --- /dev/null +++ b/management/client/rest/geo.go @@ -0,0 +1,36 @@ +package rest + +import ( + "context" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// GeoLocationAPI APIs for Geo-Location, do not use directly +type GeoLocationAPI struct { + c *Client +} + +// ListCountries list all country codes +// See more: https://docs.netbird.io/api/resources/geo-locations#list-all-country-codes +func (a *GeoLocationAPI) ListCountries(ctx context.Context) ([]api.Country, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Country](resp) + return ret, err +} + +// ListCountryCities Get a list of all English city names for a given country code +// See more: https://docs.netbird.io/api/resources/geo-locations#list-all-city-names-by-country +func (a *GeoLocationAPI) ListCountryCities(ctx context.Context, countryCode string) ([]api.City, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/locations/countries/"+countryCode+"/cities", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.City](resp) + return ret, err +} diff --git a/management/client/rest/geo_test.go b/management/client/rest/geo_test.go new file mode 100644 index 00000000000..dd42ecba896 --- /dev/null +++ b/management/client/rest/geo_test.go @@ -0,0 +1,96 @@ +package rest + +import ( + "context" + "encoding/json" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testCountry = api.Country{ + CountryCode: "DE", + CountryName: "Germany", + } + + testCity = api.City{ + CityName: "Berlin", + GeonameId: 2950158, + } +) + +func TestGeo_ListCountries_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Country{testCountry}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountries(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testCountry, ret[0]) + }) +} + +func TestGeo_ListCountries_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountries(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGeo_ListCountryCities_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.City{testCity}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountryCities(context.Background(), "Test") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testCity, ret[0]) + }) +} + +func TestGeo_ListCountryCities_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/locations/countries/Test/cities", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.GeoLocation.ListCountryCities(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGeo_Integration(t *testing.T) { + // Blackbox is initialized with empty GeoLocations + withBlackBoxServer(t, func(c *Client) { + countries, err := c.GeoLocation.ListCountries(context.Background()) + require.NoError(t, err) + assert.Empty(t, countries) + + cities, err := c.GeoLocation.ListCountryCities(context.Background(), "DE") + require.NoError(t, err) + assert.Empty(t, cities) + }) +} diff --git a/management/client/rest/groups.go b/management/client/rest/groups.go new file mode 100644 index 00000000000..feb66427380 --- /dev/null +++ b/management/client/rest/groups.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// GroupsAPI APIs for Groups, do not use directly +type GroupsAPI struct { + c *Client +} + +// List list all groups +// See more: https://docs.netbird.io/api/resources/groups#list-all-groups +func (a *GroupsAPI) List(ctx context.Context) ([]api.Group, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/groups", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Group](resp) + return ret, err +} + +// Get get group info +// See more: https://docs.netbird.io/api/resources/groups#retrieve-a-group +func (a *GroupsAPI) Get(ctx context.Context, groupID string) (*api.Group, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/groups/"+groupID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Create create new group +// See more: https://docs.netbird.io/api/resources/groups#create-a-group +func (a *GroupsAPI) Create(ctx context.Context, request api.PostApiGroupsJSONRequestBody) (*api.Group, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/groups", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Update update group info +// See more: https://docs.netbird.io/api/resources/groups#update-a-group +func (a *GroupsAPI) Update(ctx context.Context, groupID string, request api.PutApiGroupsGroupIdJSONRequestBody) (*api.Group, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/groups/"+groupID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Group](resp) + return &ret, err +} + +// Delete delete group +// See more: https://docs.netbird.io/api/resources/groups#delete-a-group +func (a *GroupsAPI) Delete(ctx context.Context, groupID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/groups/"+groupID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/groups_test.go b/management/client/rest/groups_test.go new file mode 100644 index 00000000000..ac534437d24 --- /dev/null +++ b/management/client/rest/groups_test.go @@ -0,0 +1,210 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testGroup = api.Group{ + Id: "Test", + Name: "wow", + PeersCount: 0, + } +) + +func TestGroups_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Group{testGroup}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testGroup, ret[0]) + }) +} + +func TestGroups_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGroups_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testGroup) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestGroups_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiGroupsJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Create(context.Background(), api.PostApiGroupsJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Create(context.Background(), api.PostApiGroupsJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestGroups_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiGroupsGroupIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testGroup) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Update(context.Background(), "Test", api.PutApiGroupsGroupIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testGroup, *ret) + }) +} + +func TestGroups_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Groups.Update(context.Background(), "Test", api.PutApiGroupsGroupIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestGroups_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Groups.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestGroups_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/groups/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Groups.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestGroups_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + groups, err := c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, groups, 1) + + group, err := c.Groups.Create(context.Background(), api.GroupRequest{ + Name: "Test", + }) + require.NoError(t, err) + assert.Equal(t, "Test", group.Name) + assert.NotEmpty(t, group.Id) + + group, err = c.Groups.Update(context.Background(), group.Id, api.GroupRequest{ + Name: "Testnt", + }) + require.NoError(t, err) + assert.Equal(t, "Testnt", group.Name) + + err = c.Groups.Delete(context.Background(), group.Id) + require.NoError(t, err) + + groups, err = c.Groups.List(context.Background()) + require.NoError(t, err) + assert.Len(t, groups, 1) + }) +} diff --git a/management/client/rest/networks.go b/management/client/rest/networks.go new file mode 100644 index 00000000000..2cdd6d73dab --- /dev/null +++ b/management/client/rest/networks.go @@ -0,0 +1,246 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// NetworksAPI APIs for Networks, do not use directly +type NetworksAPI struct { + c *Client +} + +// List list all networks +// See more: https://docs.netbird.io/api/resources/networks#list-all-networks +func (a *NetworksAPI) List(ctx context.Context) ([]api.Network, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Network](resp) + return ret, err +} + +// Get get network info +// See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network +func (a *NetworksAPI) Get(ctx context.Context, networkID string) (*api.Network, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+networkID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Create create new network +// See more: https://docs.netbird.io/api/resources/networks#create-a-network +func (a *NetworksAPI) Create(ctx context.Context, request api.PostApiNetworksJSONRequestBody) (*api.Network, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Update update network +// See more: https://docs.netbird.io/api/resources/networks#update-a-network +func (a *NetworksAPI) Update(ctx context.Context, networkID string, request api.PutApiNetworksNetworkIdJSONRequestBody) (*api.Network, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+networkID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Network](resp) + return &ret, err +} + +// Delete delete network +// See more: https://docs.netbird.io/api/resources/networks#delete-a-network +func (a *NetworksAPI) Delete(ctx context.Context, networkID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+networkID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// NetworkResourcesAPI APIs for Network Resources, do not use directly +type NetworkResourcesAPI struct { + c *Client + networkID string +} + +// Resources APIs for network resources +func (a *NetworksAPI) Resources(networkID string) *NetworkResourcesAPI { + return &NetworkResourcesAPI{ + c: a.c, + networkID: networkID, + } +} + +// List list all resources in networks +// See more: https://docs.netbird.io/api/resources/networks#list-all-network-resources +func (a *NetworkResourcesAPI) List(ctx context.Context) ([]api.NetworkResource, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NetworkResource](resp) + return ret, err +} + +// Get get network resource info +// See more: https://docs.netbird.io/api/resources/networks#retrieve-a-network-resource +func (a *NetworkResourcesAPI) Get(ctx context.Context, networkResourceID string) (*api.NetworkResource, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Create create new network resource +// See more: https://docs.netbird.io/api/resources/networks#create-a-network-resource +func (a *NetworkResourcesAPI) Create(ctx context.Context, request api.PostApiNetworksNetworkIdResourcesJSONRequestBody) (*api.NetworkResource, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/resources", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Update update network resource +// See more: https://docs.netbird.io/api/resources/networks#update-a-network-resource +func (a *NetworkResourcesAPI) Update(ctx context.Context, networkResourceID string, request api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody) (*api.NetworkResource, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkResource](resp) + return &ret, err +} + +// Delete delete network resource +// See more: https://docs.netbird.io/api/resources/networks#delete-a-network-resource +func (a *NetworkResourcesAPI) Delete(ctx context.Context, networkResourceID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/resources/"+networkResourceID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// NetworkRoutersAPI APIs for Network Routers, do not use directly +type NetworkRoutersAPI struct { + c *Client + networkID string +} + +// Routers APIs for network routers +func (a *NetworksAPI) Routers(networkID string) *NetworkRoutersAPI { + return &NetworkRoutersAPI{ + c: a.c, + networkID: networkID, + } +} + +// List list all routers in networks +// See more: https://docs.netbird.io/api/routers/networks#list-all-network-routers +func (a *NetworkRoutersAPI) List(ctx context.Context) ([]api.NetworkRouter, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.NetworkRouter](resp) + return ret, err +} + +// Get get network router info +// See more: https://docs.netbird.io/api/routers/networks#retrieve-a-network-router +func (a *NetworkRoutersAPI) Get(ctx context.Context, networkRouterID string) (*api.NetworkRouter, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Create create new network router +// See more: https://docs.netbird.io/api/routers/networks#create-a-network-router +func (a *NetworkRoutersAPI) Create(ctx context.Context, request api.PostApiNetworksNetworkIdRoutersJSONRequestBody) (*api.NetworkRouter, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/networks/"+a.networkID+"/routers", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Update update network router +// See more: https://docs.netbird.io/api/routers/networks#update-a-network-router +func (a *NetworkRoutersAPI) Update(ctx context.Context, networkRouterID string, request api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody) (*api.NetworkRouter, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.NetworkRouter](resp) + return &ret, err +} + +// Delete delete network router +// See more: https://docs.netbird.io/api/routers/networks#delete-a-network-router +func (a *NetworkRoutersAPI) Delete(ctx context.Context, networkRouterID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/networks/"+a.networkID+"/routers/"+networkRouterID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/networks_test.go b/management/client/rest/networks_test.go new file mode 100644 index 00000000000..934c55380bc --- /dev/null +++ b/management/client/rest/networks_test.go @@ -0,0 +1,589 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testNetwork = api.Network{ + Id: "Test", + Name: "wow", + } + + testNetworkResource = api.NetworkResource{ + Description: ptr("meaw"), + Id: "awa", + } + + testNetworkRouter = api.NetworkRouter{ + Id: "ouch", + } +) + +func TestNetworks_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Network{testNetwork}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetwork, ret[0]) + }) +} + +func TestNetworks_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworks_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetwork) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworks_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetwork) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Create(context.Background(), api.PostApiNetworksJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Create(context.Background(), api.PostApiNetworksJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworks_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetwork) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Update(context.Background(), "Test", api.PutApiNetworksNetworkIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetwork, *ret) + }) +} + +func TestNetworks_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Update(context.Background(), "Test", api.PutApiNetworksNetworkIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworks_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworks_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworks_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + network, err := c.Networks.Create(context.Background(), api.NetworkRequest{ + Description: ptr("TestNetwork"), + Name: "Test", + }) + assert.NoError(t, err) + assert.Equal(t, "Test", network.Name) + + networks, err := c.Networks.List(context.Background()) + assert.NoError(t, err) + assert.Empty(t, networks) + + network, err = c.Networks.Update(context.Background(), "TestID", api.NetworkRequest{ + Description: ptr("TestNetwork?"), + Name: "Test", + }) + + assert.NoError(t, err) + assert.Equal(t, "TestNetwork?", *network.Description) + + err = c.Networks.Delete(context.Background(), "TestID") + assert.NoError(t, err) + }) +} + +func TestNetworkResources_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NetworkResource{testNetworkResource}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetworkResource, ret[0]) + }) +} + +func TestNetworkResources_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkResources_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetworkResource) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkResources_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksNetworkIdResourcesJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetworkResource) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdResourcesJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdResourcesJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkResources_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testNetworkResource) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testNetworkResource, *ret) + }) +} + +func TestNetworkResources_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Resources("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdResourcesResourceIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkResources_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Resources("Meow").Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworkResources_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/resources/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Resources("Meow").Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworkResources_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + _, err := c.Networks.Resources("TestNetwork").Create(context.Background(), api.NetworkResourceRequest{ + Address: "test.com", + Description: ptr("Description"), + Enabled: false, + Groups: []string{"test"}, + Name: "test", + }) + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").List(context.Background()) + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").Get(context.Background(), "TestResource") + assert.NoError(t, err) + + _, err = c.Networks.Resources("TestNetwork").Update(context.Background(), "TestResource", api.NetworkResourceRequest{ + Address: "testnt.com", + }) + assert.NoError(t, err) + + err = c.Networks.Resources("TestNetwork").Delete(context.Background(), "TestResource") + assert.NoError(t, err) + }) +} + +func TestNetworkRouters_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.NetworkRouter{testNetworkRouter}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testNetworkRouter, ret[0]) + }) +} + +func TestNetworkRouters_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkRouters_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testNetworkRouter) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestNetworkRouters_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiNetworksNetworkIdRoutersJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "test", *req.Peer) + retBytes, _ := json.Marshal(testNetworkRouter) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdRoutersJSONRequestBody{ + Peer: ptr("test"), + }) + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Create(context.Background(), api.PostApiNetworksNetworkIdRoutersJSONRequestBody{ + Peer: ptr("test"), + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkRouters_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "test", *req.Peer) + retBytes, _ := json.Marshal(testNetworkRouter) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody{ + Peer: ptr("test"), + }) + require.NoError(t, err) + assert.Equal(t, testNetworkRouter, *ret) + }) +} + +func TestNetworkRouters_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Networks.Routers("Meow").Update(context.Background(), "Test", api.PutApiNetworksNetworkIdRoutersRouterIdJSONRequestBody{ + Peer: ptr("test"), + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestNetworkRouters_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Networks.Routers("Meow").Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestNetworkRouters_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/networks/Meow/routers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Networks.Routers("Meow").Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestNetworkRouters_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + _, err := c.Networks.Routers("TestNetwork").Create(context.Background(), api.NetworkRouterRequest{ + Enabled: false, + Masquerade: false, + Metric: 9999, + PeerGroups: ptr([]string{"test"}), + }) + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").List(context.Background()) + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").Get(context.Background(), "TestRouter") + assert.NoError(t, err) + + _, err = c.Networks.Routers("TestNetwork").Update(context.Background(), "TestRouter", api.NetworkRouterRequest{ + Enabled: true, + }) + assert.NoError(t, err) + + err = c.Networks.Routers("TestNetwork").Delete(context.Background(), "TestRouter") + assert.NoError(t, err) + }) +} diff --git a/management/client/rest/peers.go b/management/client/rest/peers.go new file mode 100644 index 00000000000..9d35f013cba --- /dev/null +++ b/management/client/rest/peers.go @@ -0,0 +1,78 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PeersAPI APIs for peers, do not use directly +type PeersAPI struct { + c *Client +} + +// List list all peers +// See more: https://docs.netbird.io/api/resources/peers#list-all-peers +func (a *PeersAPI) List(ctx context.Context) ([]api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Peer](resp) + return ret, err +} + +// Get retrieve a peer +// See more: https://docs.netbird.io/api/resources/peers#retrieve-a-peer +func (a *PeersAPI) Get(ctx context.Context, peerID string) (*api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Peer](resp) + return &ret, err +} + +// Update update information for a peer +// See more: https://docs.netbird.io/api/resources/peers#update-a-peer +func (a *PeersAPI) Update(ctx context.Context, peerID string, request api.PutApiPeersPeerIdJSONRequestBody) (*api.Peer, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/peers/"+peerID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Peer](resp) + return &ret, err +} + +// Delete delete a peer +// See more: https://docs.netbird.io/api/resources/peers#delete-a-peer +func (a *PeersAPI) Delete(ctx context.Context, peerID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/peers/"+peerID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// ListAccessiblePeers list all peers that the specified peer can connect to within the network +// See more: https://docs.netbird.io/api/resources/peers#list-accessible-peers +func (a *PeersAPI) ListAccessiblePeers(ctx context.Context, peerID string) ([]api.Peer, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/peers/"+peerID+"/accessible-peers", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Peer](resp) + return ret, err +} diff --git a/management/client/rest/peers_test.go b/management/client/rest/peers_test.go new file mode 100644 index 00000000000..216ee990c0d --- /dev/null +++ b/management/client/rest/peers_test.go @@ -0,0 +1,203 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testPeer = api.Peer{ + ApprovalRequired: false, + Connected: false, + ConnectionIp: "127.0.0.1", + DnsLabel: "test", + Id: "Test", + } +) + +func TestPeers_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Peer{testPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPeer, ret[0]) + }) +} + +func TestPeers_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPeer) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPeer, *ret) + }) +} + +func TestPeers_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPeersPeerIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.InactivityExpirationEnabled) + retBytes, _ := json.Marshal(testPeer) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Update(context.Background(), "Test", api.PutApiPeersPeerIdJSONRequestBody{ + InactivityExpirationEnabled: true, + }) + require.NoError(t, err) + assert.Equal(t, testPeer, *ret) + }) +} + +func TestPeers_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.Update(context.Background(), "Test", api.PutApiPeersPeerIdJSONRequestBody{ + InactivityExpirationEnabled: false, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPeers_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Peers.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPeers_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Peers.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPeers_ListAccessiblePeers_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Peer{testPeer}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.ListAccessiblePeers(context.Background(), "Test") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPeer, ret[0]) + }) +} + +func TestPeers_ListAccessiblePeers_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/peers/Test/accessible-peers", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Peers.ListAccessiblePeers(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPeers_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + peers, err := c.Peers.List(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, peers) + + peer, err := c.Peers.Get(context.Background(), peers[0].Id) + require.NoError(t, err) + assert.Equal(t, peers[0].Id, peer.Id) + + peer, err = c.Peers.Update(context.Background(), peer.Id, api.PeerRequest{ + LoginExpirationEnabled: true, + Name: "Test", + SshEnabled: false, + ApprovalRequired: ptr(false), + InactivityExpirationEnabled: false, + }) + require.NoError(t, err) + assert.Equal(t, true, peer.LoginExpirationEnabled) + + accessiblePeers, err := c.Peers.ListAccessiblePeers(context.Background(), peer.Id) + require.NoError(t, err) + assert.Empty(t, accessiblePeers) + + err = c.Peers.Delete(context.Background(), peer.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/policies.go b/management/client/rest/policies.go new file mode 100644 index 00000000000..be6abafaf56 --- /dev/null +++ b/management/client/rest/policies.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PoliciesAPI APIs for Policies, do not use directly +type PoliciesAPI struct { + c *Client +} + +// List list all policies +// See more: https://docs.netbird.io/api/resources/policies#list-all-policies +func (a *PoliciesAPI) List(ctx context.Context) ([]api.Policy, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/policies", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Policy](resp) + return ret, err +} + +// Get get policy info +// See more: https://docs.netbird.io/api/resources/policies#retrieve-a-policy +func (a *PoliciesAPI) Get(ctx context.Context, policyID string) (*api.Policy, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/policies/"+policyID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Create create new policy +// See more: https://docs.netbird.io/api/resources/policies#create-a-policy +func (a *PoliciesAPI) Create(ctx context.Context, request api.PostApiPoliciesJSONRequestBody) (*api.Policy, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/policies", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Update update policy info +// See more: https://docs.netbird.io/api/resources/policies#update-a-policy +func (a *PoliciesAPI) Update(ctx context.Context, policyID string, request api.PutApiPoliciesPolicyIdJSONRequestBody) (*api.Policy, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/policies/"+policyID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Policy](resp) + return &ret, err +} + +// Delete delete policy +// See more: https://docs.netbird.io/api/resources/policies#delete-a-policy +func (a *PoliciesAPI) Delete(ctx context.Context, policyID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/policies/"+policyID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/policies_test.go b/management/client/rest/policies_test.go new file mode 100644 index 00000000000..f7fc6ff10a6 --- /dev/null +++ b/management/client/rest/policies_test.go @@ -0,0 +1,236 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testPolicy = api.Policy{ + Name: "wow", + Id: ptr("Test"), + Enabled: false, + } +) + +func TestPolicies_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Policy{testPolicy}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPolicy, ret[0]) + }) +} + +func TestPolicies_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPolicies_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPolicy) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPolicies_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPoliciesPolicyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPolicy) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Create(context.Background(), api.PostApiPoliciesJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Create(context.Background(), api.PostApiPoliciesJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPolicies_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiPoliciesPolicyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPolicy) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Update(context.Background(), "Test", api.PutApiPoliciesPolicyIdJSONRequestBody{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPolicy, *ret) + }) +} + +func TestPolicies_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Policies.Update(context.Background(), "Test", api.PutApiPoliciesPolicyIdJSONRequestBody{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPolicies_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Policies.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPolicies_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/policies/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Policies.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPolicies_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + policies, err := c.Policies.List(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, policies) + + policy, err := c.Policies.Get(context.Background(), *policies[0].Id) + require.NoError(t, err) + assert.Equal(t, *policies[0].Id, *policy.Id) + + policy, err = c.Policies.Update(context.Background(), *policy.Id, api.PolicyCreate{ + Description: ptr("Test Policy"), + Enabled: false, + Name: "Test", + Rules: []api.PolicyRuleUpdate{ + { + Action: api.PolicyRuleUpdateAction(policy.Rules[0].Action), + Bidirectional: true, + Description: ptr("Test Policy"), + Sources: ptr([]string{(*policy.Rules[0].Sources)[0].Id}), + Destinations: ptr([]string{(*policy.Rules[0].Destinations)[0].Id}), + Enabled: false, + Protocol: api.PolicyRuleUpdateProtocolAll, + }, + }, + SourcePostureChecks: nil, + }) + require.NoError(t, err) + assert.Equal(t, "Test Policy", *policy.Rules[0].Description) + + policy, err = c.Policies.Create(context.Background(), api.PolicyUpdate{ + Description: ptr("Test Policy 2"), + Enabled: false, + Name: "Test", + Rules: []api.PolicyRuleUpdate{ + { + Action: api.PolicyRuleUpdateAction(policy.Rules[0].Action), + Bidirectional: true, + Description: ptr("Test Policy 2"), + Sources: ptr([]string{(*policy.Rules[0].Sources)[0].Id}), + Destinations: ptr([]string{(*policy.Rules[0].Destinations)[0].Id}), + Enabled: false, + Protocol: api.PolicyRuleUpdateProtocolAll, + }, + }, + SourcePostureChecks: nil, + }) + require.NoError(t, err) + + err = c.Policies.Delete(context.Background(), *policy.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/posturechecks.go b/management/client/rest/posturechecks.go new file mode 100644 index 00000000000..950d17ba05f --- /dev/null +++ b/management/client/rest/posturechecks.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// PostureChecksAPI APIs for PostureChecks, do not use directly +type PostureChecksAPI struct { + c *Client +} + +// List list all posture checks +// See more: https://docs.netbird.io/api/resources/posture-checks#list-all-posture-checks +func (a *PostureChecksAPI) List(ctx context.Context) ([]api.PostureCheck, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.PostureCheck](resp) + return ret, err +} + +// Get get posture check info +// See more: https://docs.netbird.io/api/resources/posture-checks#retrieve-a-posture-check +func (a *PostureChecksAPI) Get(ctx context.Context, postureCheckID string) (*api.PostureCheck, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/posture-checks/"+postureCheckID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Create create new posture check +// See more: https://docs.netbird.io/api/resources/posture-checks#create-a-posture-check +func (a *PostureChecksAPI) Create(ctx context.Context, request api.PostApiPostureChecksJSONRequestBody) (*api.PostureCheck, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/posture-checks", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Update update posture check info +// See more: https://docs.netbird.io/api/resources/posture-checks#update-a-posture-check +func (a *PostureChecksAPI) Update(ctx context.Context, postureCheckID string, request api.PutApiPostureChecksPostureCheckIdJSONRequestBody) (*api.PostureCheck, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/posture-checks/"+postureCheckID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PostureCheck](resp) + return &ret, err +} + +// Delete delete posture check +// See more: https://docs.netbird.io/api/resources/posture-checks#delete-a-posture-check +func (a *PostureChecksAPI) Delete(ctx context.Context, postureCheckID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/posture-checks/"+postureCheckID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/posturechecks_test.go b/management/client/rest/posturechecks_test.go new file mode 100644 index 00000000000..6fefc01407e --- /dev/null +++ b/management/client/rest/posturechecks_test.go @@ -0,0 +1,228 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testPostureCheck = api.PostureCheck{ + Id: "Test", + Name: "wow", + } +) + +func TestPostureChecks_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.PostureCheck{testPostureCheck}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testPostureCheck, ret[0]) + }) +} + +func TestPostureChecks_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPostureChecks_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testPostureCheck) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestPostureChecks_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostureCheckUpdate + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPostureCheck) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPostureChecks_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostureCheckUpdate + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "weaw", req.Name) + retBytes, _ := json.Marshal(testPostureCheck) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Update(context.Background(), "Test", api.PostureCheckUpdate{ + Name: "weaw", + }) + require.NoError(t, err) + assert.Equal(t, testPostureCheck, *ret) + }) +} + +func TestPostureChecks_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.PostureChecks.Update(context.Background(), "Test", api.PostureCheckUpdate{ + Name: "weaw", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestPostureChecks_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.PostureChecks.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestPostureChecks_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/posture-checks/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.PostureChecks.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestPostureChecks_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + check, err := c.PostureChecks.Create(context.Background(), api.PostureCheckUpdate{ + Name: "Test", + Description: "Testing", + Checks: &api.Checks{ + OsVersionCheck: &api.OSVersionCheck{ + Windows: &api.MinKernelVersionCheck{ + MinKernelVersion: "0.0.0", + }, + }, + }, + }) + require.NoError(t, err) + assert.Equal(t, "Test", check.Name) + + checks, err := c.PostureChecks.List(context.Background()) + require.NoError(t, err) + assert.Len(t, checks, 1) + + check, err = c.PostureChecks.Update(context.Background(), check.Id, api.PostureCheckUpdate{ + Name: "Tests", + Description: "Testings", + Checks: &api.Checks{ + GeoLocationCheck: &api.GeoLocationCheck{ + Action: api.GeoLocationCheckActionAllow, Locations: []api.Location{ + { + CityName: ptr("Cairo"), + CountryCode: "EG", + }, + }, + }, + }, + }) + + require.NoError(t, err) + assert.Equal(t, "Testings", *check.Description) + + check, err = c.PostureChecks.Get(context.Background(), check.Id) + require.NoError(t, err) + assert.Equal(t, "Tests", check.Name) + + err = c.PostureChecks.Delete(context.Background(), check.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/routes.go b/management/client/rest/routes.go new file mode 100644 index 00000000000..bccbb8847fd --- /dev/null +++ b/management/client/rest/routes.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// RoutesAPI APIs for Routes, do not use directly +type RoutesAPI struct { + c *Client +} + +// List list all routes +// See more: https://docs.netbird.io/api/resources/routes#list-all-routes +func (a *RoutesAPI) List(ctx context.Context) ([]api.Route, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/routes", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.Route](resp) + return ret, err +} + +// Get get route info +// See more: https://docs.netbird.io/api/resources/routes#retrieve-a-route +func (a *RoutesAPI) Get(ctx context.Context, routeID string) (*api.Route, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/routes/"+routeID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Create create new route +// See more: https://docs.netbird.io/api/resources/routes#create-a-route +func (a *RoutesAPI) Create(ctx context.Context, request api.PostApiRoutesJSONRequestBody) (*api.Route, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/routes", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Update update route info +// See more: https://docs.netbird.io/api/resources/routes#update-a-route +func (a *RoutesAPI) Update(ctx context.Context, routeID string, request api.PutApiRoutesRouteIdJSONRequestBody) (*api.Route, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/routes/"+routeID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.Route](resp) + return &ret, err +} + +// Delete delete route +// See more: https://docs.netbird.io/api/resources/routes#delete-a-route +func (a *RoutesAPI) Delete(ctx context.Context, routeID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/routes/"+routeID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/routes_test.go b/management/client/rest/routes_test.go new file mode 100644 index 00000000000..123bd41d4f0 --- /dev/null +++ b/management/client/rest/routes_test.go @@ -0,0 +1,226 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testRoute = api.Route{ + Id: "Test", + Domains: ptr([]string{"google.com"}), + } +) + +func TestRoutes_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.Route{testRoute}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testRoute, ret[0]) + }) +} + +func TestRoutes_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestRoutes_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testRoute) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestRoutes_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiRoutesJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "meow", req.Description) + retBytes, _ := json.Marshal(testRoute) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Create(context.Background(), api.PostApiRoutesJSONRequestBody{ + Description: "meow", + }) + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Create(context.Background(), api.PostApiRoutesJSONRequestBody{ + Description: "meow", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestRoutes_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiRoutesRouteIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, "meow", req.Description) + retBytes, _ := json.Marshal(testRoute) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Update(context.Background(), "Test", api.PutApiRoutesRouteIdJSONRequestBody{ + Description: "meow", + }) + require.NoError(t, err) + assert.Equal(t, testRoute, *ret) + }) +} + +func TestRoutes_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Routes.Update(context.Background(), "Test", api.PutApiRoutesRouteIdJSONRequestBody{ + Description: "meow", + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestRoutes_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Routes.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestRoutes_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/routes/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Routes.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestRoutes_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + route, err := c.Routes.Create(context.Background(), api.RouteRequest{ + Description: "Meow", + Enabled: false, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + PeerGroups: ptr([]string{"cs1tnh0hhcjnqoiuebeg"}), + Domains: ptr([]string{"google.com"}), + Masquerade: true, + Metric: 9999, + KeepRoute: false, + NetworkId: "Test", + }) + + require.NoError(t, err) + assert.Equal(t, "Test", route.NetworkId) + + routes, err := c.Routes.List(context.Background()) + require.NoError(t, err) + assert.Len(t, routes, 1) + + route, err = c.Routes.Update(context.Background(), route.Id, api.RouteRequest{ + Description: "Testings", + Enabled: false, + Groups: []string{"cs1tnh0hhcjnqoiuebeg"}, + PeerGroups: ptr([]string{"cs1tnh0hhcjnqoiuebeg"}), + Domains: ptr([]string{"google.com"}), + Masquerade: true, + Metric: 9999, + KeepRoute: false, + NetworkId: "Tests", + }) + + require.NoError(t, err) + assert.Equal(t, "Testings", route.Description) + + route, err = c.Routes.Get(context.Background(), route.Id) + require.NoError(t, err) + assert.Equal(t, "Tests", route.NetworkId) + + err = c.Routes.Delete(context.Background(), route.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/setupkeys.go b/management/client/rest/setupkeys.go new file mode 100644 index 00000000000..645614fcf39 --- /dev/null +++ b/management/client/rest/setupkeys.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// SetupKeysAPI APIs for Setup keys, do not use directly +type SetupKeysAPI struct { + c *Client +} + +// List list all setup keys +// See more: https://docs.netbird.io/api/resources/setup-keys#list-all-setup-keys +func (a *SetupKeysAPI) List(ctx context.Context) ([]api.SetupKey, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.SetupKey](resp) + return ret, err +} + +// Get get setup key info +// See more: https://docs.netbird.io/api/resources/setup-keys#retrieve-a-setup-key +func (a *SetupKeysAPI) Get(ctx context.Context, setupKeyID string) (*api.SetupKey, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/setup-keys/"+setupKeyID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKey](resp) + return &ret, err +} + +// Create generate new Setup Key +// See more: https://docs.netbird.io/api/resources/setup-keys#create-a-setup-key +func (a *SetupKeysAPI) Create(ctx context.Context, request api.PostApiSetupKeysJSONRequestBody) (*api.SetupKeyClear, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/setup-keys", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKeyClear](resp) + return &ret, err +} + +// Update generate new Setup Key +// See more: https://docs.netbird.io/api/resources/setup-keys#update-a-setup-key +func (a *SetupKeysAPI) Update(ctx context.Context, setupKeyID string, request api.PutApiSetupKeysKeyIdJSONRequestBody) (*api.SetupKey, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/setup-keys/"+setupKeyID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.SetupKey](resp) + return &ret, err +} + +// Delete delete setup key +// See more: https://docs.netbird.io/api/resources/setup-keys#delete-a-setup-key +func (a *SetupKeysAPI) Delete(ctx context.Context, setupKeyID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/setup-keys/"+setupKeyID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/setupkeys_test.go b/management/client/rest/setupkeys_test.go new file mode 100644 index 00000000000..82c3d1fc8a2 --- /dev/null +++ b/management/client/rest/setupkeys_test.go @@ -0,0 +1,227 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testSetupKey = api.SetupKey{ + Id: "Test", + Name: "wow", + AutoGroups: []string{"meow"}, + Ephemeral: true, + } + + testSteupKeyGenerated = api.SetupKeyClear{ + Id: "Test", + Name: "wow", + AutoGroups: []string{"meow"}, + Ephemeral: true, + Key: "shhh", + } +) + +func TestSetupKeys_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.SetupKey{testSetupKey}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testSetupKey, ret[0]) + }) +} + +func TestSetupKeys_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestSetupKeys_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testSetupKey) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Get(context.Background(), "Test") + require.NoError(t, err) + assert.Equal(t, testSetupKey, *ret) + }) +} + +func TestSetupKeys_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Get(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestSetupKeys_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiSetupKeysJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, 5, req.ExpiresIn) + retBytes, _ := json.Marshal(testSteupKeyGenerated) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Create(context.Background(), api.PostApiSetupKeysJSONRequestBody{ + ExpiresIn: 5, + }) + require.NoError(t, err) + assert.Equal(t, testSteupKeyGenerated, *ret) + }) +} + +func TestSetupKeys_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Create(context.Background(), api.PostApiSetupKeysJSONRequestBody{ + ExpiresIn: 5, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSetupKeys_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiSetupKeysKeyIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.Revoked) + retBytes, _ := json.Marshal(testSetupKey) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Update(context.Background(), "Test", api.PutApiSetupKeysKeyIdJSONRequestBody{ + Revoked: true, + }) + require.NoError(t, err) + assert.Equal(t, testSetupKey, *ret) + }) +} + +func TestSetupKeys_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.SetupKeys.Update(context.Background(), "Test", api.PutApiSetupKeysKeyIdJSONRequestBody{ + Revoked: true, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestSetupKeys_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.SetupKeys.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestSetupKeys_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/setup-keys/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.SetupKeys.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestSetupKeys_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + group, err := c.Groups.Create(context.Background(), api.GroupRequest{ + Name: "Test", + }) + require.NoError(t, err) + + skClear, err := c.SetupKeys.Create(context.Background(), api.CreateSetupKeyRequest{ + AutoGroups: []string{group.Id}, + Ephemeral: ptr(false), + Name: "test", + Type: "reusable", + }) + + require.NoError(t, err) + assert.Equal(t, true, skClear.Valid) + + keys, err := c.SetupKeys.List(context.Background()) + require.NoError(t, err) + assert.Len(t, keys, 2) + + sk, err := c.SetupKeys.Update(context.Background(), skClear.Id, api.SetupKeyRequest{ + Revoked: true, + AutoGroups: []string{group.Id}, + }) + require.NoError(t, err) + + sk, err = c.SetupKeys.Get(context.Background(), sk.Id) + require.NoError(t, err) + assert.Equal(t, false, sk.Valid) + + err = c.SetupKeys.Delete(context.Background(), sk.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/tokens.go b/management/client/rest/tokens.go new file mode 100644 index 00000000000..3275bea81e1 --- /dev/null +++ b/management/client/rest/tokens.go @@ -0,0 +1,66 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// TokensAPI APIs for PATs, do not use directly +type TokensAPI struct { + c *Client +} + +// List list user tokens +// See more: https://docs.netbird.io/api/resources/tokens#list-all-tokens +func (a *TokensAPI) List(ctx context.Context, userID string) ([]api.PersonalAccessToken, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.PersonalAccessToken](resp) + return ret, err +} + +// Get get user token info +// See more: https://docs.netbird.io/api/resources/tokens#retrieve-a-token +func (a *TokensAPI) Get(ctx context.Context, userID, tokenID string) (*api.PersonalAccessToken, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users/"+userID+"/tokens/"+tokenID, nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PersonalAccessToken](resp) + return &ret, err +} + +// Create generate new PAT for user +// See more: https://docs.netbird.io/api/resources/tokens#create-a-token +func (a *TokensAPI) Create(ctx context.Context, userID string, request api.PostApiUsersUserIdTokensJSONRequestBody) (*api.PersonalAccessTokenGenerated, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/tokens", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.PersonalAccessTokenGenerated](resp) + return &ret, err +} + +// Delete delete user token +// See more: https://docs.netbird.io/api/resources/tokens#delete-a-token +func (a *TokensAPI) Delete(ctx context.Context, userID, tokenID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID+"/tokens/"+tokenID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/tokens_test.go b/management/client/rest/tokens_test.go new file mode 100644 index 00000000000..478fae93e1a --- /dev/null +++ b/management/client/rest/tokens_test.go @@ -0,0 +1,175 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testToken = api.PersonalAccessToken{ + Id: "Test", + CreatedAt: time.Time{}, + CreatedBy: "meow", + ExpirationDate: time.Time{}, + LastUsed: nil, + Name: "wow", + } + + testTokenGenerated = api.PersonalAccessTokenGenerated{ + PersonalAccessToken: testToken, + PlainToken: "shhh", + } +) + +func TestTokens_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.PersonalAccessToken{testToken}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.List(context.Background(), "meow") + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testToken, ret[0]) + }) +} + +func TestTokens_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.List(context.Background(), "meow") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestTokens_Get_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(testToken) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Get(context.Background(), "meow", "Test") + require.NoError(t, err) + assert.Equal(t, testToken, *ret) + }) +} + +func TestTokens_Get_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Get(context.Background(), "meow", "Test") + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestTokens_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiUsersUserIdTokensJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, 5, req.ExpiresIn) + retBytes, _ := json.Marshal(testTokenGenerated) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Create(context.Background(), "meow", api.PostApiUsersUserIdTokensJSONRequestBody{ + ExpiresIn: 5, + }) + require.NoError(t, err) + assert.Equal(t, testTokenGenerated, *ret) + }) +} + +func TestTokens_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Tokens.Create(context.Background(), "meow", api.PostApiUsersUserIdTokensJSONRequestBody{ + ExpiresIn: 5, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestTokens_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Tokens.Delete(context.Background(), "meow", "Test") + require.NoError(t, err) + }) +} + +func TestTokens_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/meow/tokens/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Tokens.Delete(context.Background(), "meow", "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestTokens_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + tokenClear, err := c.Tokens.Create(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", api.PersonalAccessTokenRequest{ + Name: "Test", + ExpiresIn: 365, + }) + + require.NoError(t, err) + assert.Equal(t, "Test", tokenClear.PersonalAccessToken.Name) + + tokens, err := c.Tokens.List(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003") + require.NoError(t, err) + assert.Len(t, tokens, 2) + + token, err := c.Tokens.Get(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", tokenClear.PersonalAccessToken.Id) + require.NoError(t, err) + assert.Equal(t, "Test", token.Name) + + err = c.Tokens.Delete(context.Background(), "a23efe53-63fb-11ec-90d6-0242ac120003", token.Id) + require.NoError(t, err) + }) +} diff --git a/management/client/rest/users.go b/management/client/rest/users.go new file mode 100644 index 00000000000..372bcee455b --- /dev/null +++ b/management/client/rest/users.go @@ -0,0 +1,82 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + + "github.com/netbirdio/netbird/management/server/http/api" +) + +// UsersAPI APIs for users, do not use directly +type UsersAPI struct { + c *Client +} + +// List list all users, only returns one user always +// See more: https://docs.netbird.io/api/resources/users#list-all-users +func (a *UsersAPI) List(ctx context.Context) ([]api.User, error) { + resp, err := a.c.newRequest(ctx, "GET", "/api/users", nil) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[[]api.User](resp) + return ret, err +} + +// Create create user +// See more: https://docs.netbird.io/api/resources/users#create-a-user +func (a *UsersAPI) Create(ctx context.Context, request api.PostApiUsersJSONRequestBody) (*api.User, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "POST", "/api/users", bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.User](resp) + return &ret, err +} + +// Update update user settings +// See more: https://docs.netbird.io/api/resources/users#update-a-user +func (a *UsersAPI) Update(ctx context.Context, userID string, request api.PutApiUsersUserIdJSONRequestBody) (*api.User, error) { + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + resp, err := a.c.newRequest(ctx, "PUT", "/api/users/"+userID, bytes.NewReader(requestBytes)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + ret, err := parseResponse[api.User](resp) + return &ret, err +} + +// Delete delete user +// See more: https://docs.netbird.io/api/resources/users#delete-a-user +func (a *UsersAPI) Delete(ctx context.Context, userID string) error { + resp, err := a.c.newRequest(ctx, "DELETE", "/api/users/"+userID, nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// ResendInvitation resend user invitation +// See more: https://docs.netbird.io/api/resources/users#resend-user-invitation +func (a *UsersAPI) ResendInvitation(ctx context.Context, userID string) error { + resp, err := a.c.newRequest(ctx, "POST", "/api/users/"+userID+"/invite", nil) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} diff --git a/management/client/rest/users_test.go b/management/client/rest/users_test.go new file mode 100644 index 00000000000..aaec3bf4200 --- /dev/null +++ b/management/client/rest/users_test.go @@ -0,0 +1,222 @@ +package rest + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" + "time" + + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/http/util" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + testUser = api.User{ + Id: "Test", + AutoGroups: []string{"test-group"}, + Email: "test@test.com", + IsBlocked: false, + IsCurrent: ptr(false), + IsServiceUser: ptr(false), + Issued: ptr("api"), + LastLogin: &time.Time{}, + Name: "M. Essam", + Permissions: &api.UserPermissions{ + DashboardView: ptr(api.UserPermissionsDashboardViewFull), + }, + Role: "user", + Status: api.UserStatusActive, + } +) + +func TestUsers_List_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal([]api.User{testUser}) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.List(context.Background()) + require.NoError(t, err) + assert.Len(t, ret, 1) + assert.Equal(t, testUser, ret[0]) + }) +} + +func TestUsers_List_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.List(context.Background()) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Empty(t, ret) + }) +} + +func TestUsers_Create_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PostApiUsersJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, []string{"meow"}, req.AutoGroups) + retBytes, _ := json.Marshal(testUser) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Create(context.Background(), api.PostApiUsersJSONRequestBody{ + AutoGroups: []string{"meow"}, + }) + require.NoError(t, err) + assert.Equal(t, testUser, *ret) + }) +} + +func TestUsers_Create_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Create(context.Background(), api.PostApiUsersJSONRequestBody{ + AutoGroups: []string{"meow"}, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_Update_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + reqBytes, err := io.ReadAll(r.Body) + require.NoError(t, err) + var req api.PutApiUsersUserIdJSONRequestBody + err = json.Unmarshal(reqBytes, &req) + require.NoError(t, err) + assert.Equal(t, true, req.IsBlocked) + retBytes, _ := json.Marshal(testUser) + _, err = w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Update(context.Background(), "Test", api.PutApiUsersUserIdJSONRequestBody{ + IsBlocked: true, + }) + require.NoError(t, err) + assert.Equal(t, testUser, *ret) + }) + +} + +func TestUsers_Update_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "No", Code: 400}) + w.WriteHeader(400) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + ret, err := c.Users.Update(context.Background(), "Test", api.PutApiUsersUserIdJSONRequestBody{ + IsBlocked: true, + }) + assert.Error(t, err) + assert.Equal(t, "No", err.Error()) + assert.Nil(t, ret) + }) +} + +func TestUsers_Delete_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "DELETE", r.Method) + w.WriteHeader(200) + }) + err := c.Users.Delete(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestUsers_Delete_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.Delete(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestUsers_ResendInvitation_200(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "POST", r.Method) + w.WriteHeader(200) + }) + err := c.Users.ResendInvitation(context.Background(), "Test") + require.NoError(t, err) + }) +} + +func TestUsers_ResendInvitation_Err(t *testing.T) { + withMockClient(func(c *Client, mux *http.ServeMux) { + mux.HandleFunc("/api/users/Test/invite", func(w http.ResponseWriter, r *http.Request) { + retBytes, _ := json.Marshal(util.ErrorResponse{Message: "Not found", Code: 404}) + w.WriteHeader(404) + _, err := w.Write(retBytes) + require.NoError(t, err) + }) + err := c.Users.ResendInvitation(context.Background(), "Test") + assert.Error(t, err) + assert.Equal(t, "Not found", err.Error()) + }) +} + +func TestUsers_Integration(t *testing.T) { + withBlackBoxServer(t, func(c *Client) { + user, err := c.Users.Create(context.Background(), api.UserCreateRequest{ + AutoGroups: []string{}, + Email: ptr("test@example.com"), + IsServiceUser: true, + Name: ptr("Nobody"), + Role: "user", + }) + + require.NoError(t, err) + assert.Equal(t, "Nobody", user.Name) + + users, err := c.Users.List(context.Background()) + require.NoError(t, err) + assert.NotEmpty(t, users) + + user, err = c.Users.Update(context.Background(), user.Id, api.UserRequest{ + AutoGroups: []string{}, + Role: "admin", + }) + + require.NoError(t, err) + assert.Equal(t, "admin", user.Role) + + err = c.Users.Delete(context.Background(), user.Id) + require.NoError(t, err) + }) +} diff --git a/management/server/testdata/store.sql b/management/server/testdata/store.sql index 17f029713fe..1c0767bde6a 100644 --- a/management/server/testdata/store.sql +++ b/management/server/testdata/store.sql @@ -19,6 +19,7 @@ CREATE INDEX `idx_accounts_domain` ON `accounts`(`domain`); CREATE INDEX `idx_setup_keys_account_id` ON `setup_keys`(`account_id`); CREATE INDEX `idx_peers_key` ON `peers`(`key`); CREATE INDEX `idx_peers_account_id` ON `peers`(`account_id`); +CREATE INDEX `idx_peers_account_id_ip` ON `peers`(`account_id`,`ip`); CREATE INDEX `idx_users_account_id` ON `users`(`account_id`); CREATE INDEX `idx_personal_access_tokens_user_id` ON `personal_access_tokens`(`user_id`); CREATE INDEX `idx_groups_account_id` ON `groups`(`account_id`); @@ -39,8 +40,11 @@ CREATE INDEX `idx_networks_account_id` ON `networks`(`account_id`); INSERT INTO accounts VALUES('bf1c8084-ba50-4ce7-9439-34653001fc3b','','2024-10-02 16:03:06.778746+02:00','test.com','private',1,'af1c8024-ha40-4ce2-9418-34653101fc3c','{"IP":"100.64.0.0","Mask":"//8AAA=="}','',0,'[]',0,86400000000000,0,0,0,'',NULL,NULL,NULL); INSERT INTO "groups" VALUES('cs1tnh0hhcjnqoiuebeg','bf1c8084-ba50-4ce7-9439-34653001fc3b','All','api','[]',0,''); INSERT INTO setup_keys VALUES('','bf1c8084-ba50-4ce7-9439-34653001fc3b','A2C8E62B-38F5-4553-B31E-DD66C696CEBB','Default key','reusable','2021-08-19 20:46:20.005936822+02:00','2321-09-18 20:46:20.005936822+02:00','2021-08-19 20:46:20.005936822+02:00',0,0,NULL,'["cs1tnh0hhcjnqoiuebeg"]',0,0); +INSERT INTO users VALUES('a23efe53-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','owner',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); INSERT INTO users VALUES('edafee4e-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','admin',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); INSERT INTO users VALUES('f4f6d672-63fb-11ec-90d6-0242ac120003','bf1c8084-ba50-4ce7-9439-34653001fc3b','user',0,0,'','[]',0,NULL,'2024-10-02 16:03:06.779156+02:00','api',0,''); +-- Unhashed PAT is "nbp_apTmlmUXHSC4PKmHwtIZNaGr8eqcVI2gMURp" +INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120004','a23efe53-63fb-11ec-90d6-0242ac120003','','smJvzexPcQ3NRezrVDUmF++0XqvFvXzx8Rsn2y9r1z0=','5023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00'); INSERT INTO personal_access_tokens VALUES('9dj38s35-63fb-11ec-90d6-0242ac120003','f4f6d672-63fb-11ec-90d6-0242ac120003','','SoMeHaShEdToKeN','2023-02-27 00:00:00+00:00','user','2023-01-01 00:00:00+00:00','2023-02-01 00:00:00+00:00'); INSERT INTO installations VALUES(1,''); INSERT INTO policies VALUES('cs1tnh0hhcjnqoiuebf0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Default','This is a default rule that allows connections between all the resources',1,'[]'); @@ -48,3 +52,4 @@ INSERT INTO policy_rules VALUES('cs387mkv2d4bgq41b6n0','cs1tnh0hhcjnqoiuebf0','D INSERT INTO network_routers VALUES('ctc20ji7qv9ck2sebc80','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','cs1tnh0hhcjnqoiuebeg',NULL,0,0); INSERT INTO network_resources VALUES ('ctc4nci7qv9061u6ilfg','ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Host','192.168.1.1'); INSERT INTO networks VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','Test Network','Test Network'); +INSERT INTO peers VALUES('ct286bi7qv930dsrrug0','bf1c8084-ba50-4ce7-9439-34653001fc3b','','','192.168.0.0','','','','','','','','','','','','','','','','','test','test','2023-01-01 00:00:00+00:00',0,0,0,'a23efe53-63fb-11ec-90d6-0242ac120003','',0,0,'2023-01-01 00:00:00+00:00','2023-01-01 00:00:00+00:00',0,'','','',0);