package handler import ( "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/labstack/echo/v4" "github.com/rs/xid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/DigitalTolk/wireguard-ui/emailer" "github.com/DigitalTolk/wireguard-ui/model" "github.com/DigitalTolk/wireguard-ui/util" ) // mockEmailer implements the emailer.Emailer interface for testing type mockEmailer struct { sendCalled bool lastTo string shouldFail bool } func (m *mockEmailer) Send(toName, to, subject, content string, attachments []emailer.Attachment) error { m.sendCalled = true m.lastTo = to if m.shouldFail { return fmt.Errorf("email send failed") } return nil } func makeClient(id string) model.Client { now := time.Now().UTC() return model.Client{ ID: id, Name: "Client " + id, AllocatedIPs: []string{}, AllowedIPs: []string{}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, } } func TestAPIListClients(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Client 1", AllocatedIPs: []string{"10.252.1.2/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients", nil) c := env.echo.NewContext(req, rec) err := APIListClients(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var clients []model.ClientData parseJSON(t, rec, &clients) assert.Len(t, clients, 1) assert.Equal(t, "Client 1", clients[0].Client.Name) } func TestAPIGetClient(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() env.db.SaveClient(makeClient(id)) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/"+id, nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIGetClient(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) } func TestAPIGetClient_InvalidID(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/bad", nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("bad!") err := APIGetClient(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIGetClient_NotFound(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/"+id, nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIGetClient(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, rec.Code) } func TestAPIDeleteClient(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() env.db.SaveClient(makeClient(id)) req, rec := jsonRequest(http.MethodDelete, "/api/v1/clients/"+id, nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIDeleteClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusNoContent, rec.Code) _, getErr := env.db.GetClientByID(id, model.QRCodeSettings{}) assert.Error(t, getErr) } func TestAPIPatchClientStatus(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() env.db.SaveClient(makeClient(id)) req, rec := jsonRequest(http.MethodPatch, "/api/v1/clients/"+id+"/status", map[string]bool{"enabled": false}) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIPatchClientStatus(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) got, _ := env.db.GetClientByID(id, model.QRCodeSettings{}) assert.False(t, got.Client.Enabled) } func TestAPISuggestClientIPs(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodGet, "/api/v1/suggest-client-ips", nil) c := env.echo.NewContext(req, rec) err := APISuggestClientIPs(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var ips []string parseJSON(t, rec, &ips) assert.NotEmpty(t, ips) } func TestAPIMachineIPs(t *testing.T) { req, rec := jsonRequest(http.MethodGet, "/api/v1/machine-ips", nil) e := echo.New() c := e.NewContext(req, rec) err := APIMachineIPs()(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) } func TestAPISubnetRanges(t *testing.T) { req, rec := jsonRequest(http.MethodGet, "/api/v1/subnet-ranges", nil) e := echo.New() c := e.NewContext(req, rec) err := APISubnetRanges()(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) } func TestAPIConfigStatus(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodGet, "/api/v1/server/config-status", nil) c := env.echo.NewContext(req, rec) err := APIConfigStatus(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var result map[string]bool parseJSON(t, rec, &result) _, hasChanged := result["changed"] assert.True(t, hasChanged) } // --- APICreateClient Tests --- func TestAPICreateClient_Success(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{ "name": "New Client", "email": "client@test.com", "allocated_ips": []string{"10.252.1.50/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "use_server_dns": true, "enabled": true, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusCreated, rec.Code) var client model.Client parseJSON(t, rec, &client) assert.Equal(t, "New Client", client.Name) assert.NotEmpty(t, client.ID) assert.NotEmpty(t, client.PublicKey) assert.NotEmpty(t, client.PrivateKey) assert.NotEmpty(t, client.PresharedKey) } func TestAPICreateClient_InvalidAllocatedIPs(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{ "name": "Bad IPs", "email": "bad@test.com", "allocated_ips": []string{"192.168.99.1/32"}, // outside server range "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPICreateClient_InvalidAllowedIPs(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{ "name": "Bad Allowed", "email": "bad@test.com", "allocated_ips": []string{"10.252.1.50/32"}, "allowed_ips": []string{"not-a-cidr"}, "extra_allowed_ips": []string{}, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPICreateClient_InvalidExtraAllowedIPs(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{ "name": "Bad Extra", "email": "bad@test.com", "allocated_ips": []string{"10.252.1.50/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{"not-a-cidr"}, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPICreateClient_WithPresharedKeyDash(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{ "name": "No PSK", "email": "nopsk@test.com", "allocated_ips": []string{"10.252.1.51/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "preshared_key": "-", "enabled": true, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusCreated, rec.Code) var client model.Client parseJSON(t, rec, &client) assert.Empty(t, client.PresharedKey) } func TestAPICreateClient_DuplicateAllocatedIP(t *testing.T) { env := setupTestEnv(t) // First, create a client body1 := map[string]interface{}{ "name": "First", "email": "first@test.com", "allocated_ips": []string{"10.252.1.60/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "enabled": true, } req1, rec1 := jsonRequest(http.MethodPost, "/api/v1/clients", body1) c1 := env.echo.NewContext(req1, rec1) err := APICreateClient(env.db, env.cw)(c1) require.NoError(t, err) assert.Equal(t, http.StatusCreated, rec1.Code) // Try to create another with same IP body2 := map[string]interface{}{ "name": "Second", "email": "second@test.com", "allocated_ips": []string{"10.252.1.60/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, } req2, rec2 := jsonRequest(http.MethodPost, "/api/v1/clients", body2) c2 := env.echo.NewContext(req2, rec2) err = APICreateClient(env.db, env.cw)(c2) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec2.Code) } // --- APIUpdateClient Tests --- func TestAPIUpdateClient_Success(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Original", Email: "original@test.com", PublicKey: "origpub", PrivateKey: "origpriv", AllocatedIPs: []string{"10.252.1.70/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) body := map[string]interface{}{ "name": "Updated", "email": "try-change@test.com", "allocated_ips": []string{"10.252.1.70/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": "origpub", "preshared_key": "", "enabled": true, "use_server_dns": false, } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var client model.Client parseJSON(t, rec, &client) assert.Equal(t, "Updated", client.Name) // email is immutable — original preserved, attempted change ignored assert.Equal(t, "original@test.com", client.Email) } func TestAPIUpdateClient_InvalidID(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{"name": "test"} req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/bad", body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("bad!") err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIUpdateClient_NotFound(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() body := map[string]interface{}{ "name": "Update", "allocated_ips": []string{"10.252.1.70/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, rec.Code) } func TestAPIUpdateClient_InvalidAllowedIPs(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Orig", PublicKey: "pub1", AllocatedIPs: []string{"10.252.1.71/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) body := map[string]interface{}{ "name": "Bad update", "allocated_ips": []string{"10.252.1.71/32"}, "allowed_ips": []string{"not-cidr"}, "extra_allowed_ips": []string{}, "public_key": "pub1", } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } // --- APIDownloadClientConfig Tests --- func TestAPIDownloadClientConfig_Success(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Download Client", PublicKey: "dlpub", PrivateKey: "dlpriv", AllocatedIPs: []string{"10.252.1.80/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, UseServerDNS: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/"+id+"/config", nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIDownloadClientConfig(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) assert.Contains(t, rec.Body.String(), "# CONFIDENTIAL") assert.Contains(t, rec.Body.String(), "[Interface]") assert.Contains(t, rec.Body.String(), "PrivateKey = dlpriv") assert.Contains(t, rec.Header().Get(echo.HeaderContentDisposition), "Download Client.conf") } func TestAPIDownloadClientConfig_InvalidID(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/bad/config", nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("bad!") err := APIDownloadClientConfig(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIDownloadClientConfig_NotFound(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/"+id+"/config", nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIDownloadClientConfig(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, rec.Code) } // --- APIGetClientQRCode Tests --- func TestAPIGetClientQRCode_Success(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "QR Client", PublicKey: "qrpub", PrivateKey: "qrpriv", AllocatedIPs: []string{"10.252.1.81/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/"+id+"/qrcode", nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIGetClientQRCode(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var result map[string]string parseJSON(t, rec, &result) assert.NotEmpty(t, result["qr_code"]) assert.Contains(t, result["qr_code"], "data:image/png;base64,") } func TestAPIGetClientQRCode_InvalidID(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/bad/qrcode", nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("bad!") err := APIGetClientQRCode(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIGetClientQRCode_NotFound(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/"+id+"/qrcode", nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIGetClientQRCode(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, rec.Code) } // --- APIApplyServerConfig Tests --- func TestAPIApplyServerConfig_Success(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodPost, "/api/v1/server/apply-config", nil) c := env.echo.NewContext(req, rec) err := APIApplyServerConfig(env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) } // --- APIPatchClientStatus edge cases --- func TestAPIPatchClientStatus_InvalidID(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodPatch, "/api/v1/clients/bad/status", map[string]bool{"enabled": true}) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("bad!") err := APIPatchClientStatus(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIPatchClientStatus_NotFound(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() req, rec := jsonRequest(http.MethodPatch, "/api/v1/clients/"+id+"/status", map[string]bool{"enabled": true}) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIPatchClientStatus(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, rec.Code) } func TestAPIPatchClientStatus_Enable(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Disabled", PublicKey: "pub1", AllocatedIPs: []string{}, AllowedIPs: []string{}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: false, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodPatch, "/api/v1/clients/"+id+"/status", map[string]bool{"enabled": true}) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIPatchClientStatus(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) got, _ := env.db.GetClientByID(id, model.QRCodeSettings{}) assert.True(t, got.Client.Enabled) } // --- APIDeleteClient edge cases --- func TestAPIDeleteClient_InvalidID(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodDelete, "/api/v1/clients/bad", nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("bad!") err := APIDeleteClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } // --- APIEmailClient Tests --- func TestAPIEmailClient_Success(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Email Client", PublicKey: "emailpub", PrivateKey: "emailpriv", AllocatedIPs: []string{"10.252.1.90/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) mailer := &mockEmailer{} body := map[string]string{"email": "recipient@test.com"} req, rec := jsonRequest(http.MethodPost, "/api/v1/clients/"+id+"/email", body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIEmailClient(env.db, mailer, "WireGuard Config", "Your config is attached.")(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) assert.True(t, mailer.sendCalled) assert.Equal(t, "recipient@test.com", mailer.lastTo) } func TestAPIEmailClient_InvalidID(t *testing.T) { env := setupTestEnv(t) mailer := &mockEmailer{} body := map[string]string{"email": "test@test.com"} req, rec := jsonRequest(http.MethodPost, "/api/v1/clients/bad/email", body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues("bad!") err := APIEmailClient(env.db, mailer, "", "")(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.False(t, mailer.sendCalled) } func TestAPIEmailClient_NotFound(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() mailer := &mockEmailer{} body := map[string]string{"email": "test@test.com"} req, rec := jsonRequest(http.MethodPost, "/api/v1/clients/"+id+"/email", body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIEmailClient(env.db, mailer, "", "")(c) require.NoError(t, err) assert.Equal(t, http.StatusNotFound, rec.Code) } func TestAPIEmailClient_SendFailure(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Fail Client", PublicKey: "fpub", PrivateKey: "fpriv", AllocatedIPs: []string{"10.252.1.91/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) mailer := &mockEmailer{shouldFail: true} body := map[string]string{"email": "fail@test.com"} req, rec := jsonRequest(http.MethodPost, "/api/v1/clients/"+id+"/email", body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIEmailClient(env.db, mailer, "Subject", "Body")(c) require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, rec.Code) } func TestAPIEmailClient_NoPrivateKey(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() // Client with no private key (external public key provided) env.db.SaveClient(model.Client{ ID: id, Name: "NoPK Client", PublicKey: "nopkpub", PrivateKey: "", AllocatedIPs: []string{"10.252.1.92/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) mailer := &mockEmailer{} body := map[string]string{"email": "nopk@test.com"} req, rec := jsonRequest(http.MethodPost, "/api/v1/clients/"+id+"/email", body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIEmailClient(env.db, mailer, "Subject", "Body")(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) assert.True(t, mailer.sendCalled) } // --- APIUpdateClient extra edge cases --- func TestAPIUpdateClient_InvalidExtraAllowedIPs(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Orig", PublicKey: "pub1", AllocatedIPs: []string{"10.252.1.73/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) body := map[string]interface{}{ "name": "Bad extra", "allocated_ips": []string{"10.252.1.73/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{"not-cidr"}, "public_key": "pub1", } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } // --- APICreateClient with provided public key --- func TestAPICreateClient_WithProvidedPublicKey(t *testing.T) { env := setupTestEnv(t) // Generate a real key pair for a valid public key body := map[string]interface{}{ "name": "External Key Client", "email": "ext@test.com", "allocated_ips": []string{"10.252.1.52/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": "YW52YWxpZGtleWJ1dGVub3VnaGJ5dGVzYWF6enp6eg==", // valid base64 but not a valid WG key "enabled": true, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) // Invalid WG key should return bad request assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPICreateClient_WithInvalidPresharedKey(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{ "name": "Bad PSK", "email": "psk@test.com", "allocated_ips": []string{"10.252.1.53/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "preshared_key": "not-a-valid-key", "enabled": true, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } // --- APIUpdateClient with changed public key --- func TestAPIUpdateClient_ChangePublicKey_Invalid(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Orig", PublicKey: "origpub", PrivateKey: "origpriv", AllocatedIPs: []string{"10.252.1.74/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) body := map[string]interface{}{ "name": "Change Key", "allocated_ips": []string{"10.252.1.74/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": "invalid-new-key", "enabled": true, } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIUpdateClient_ChangePresharedKey_Invalid(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Orig", PublicKey: "origpub", PresharedKey: "origpsk", AllocatedIPs: []string{"10.252.1.75/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) body := map[string]interface{}{ "name": "Change PSK", "allocated_ips": []string{"10.252.1.75/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": "origpub", "preshared_key": "invalid-psk", "enabled": true, } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPICreateClient_InvalidBody(t *testing.T) { env := setupTestEnv(t) // Send non-JSON body with JSON content-type to trigger bind error req := httptest.NewRequest(http.MethodPost, "/api/v1/clients", strings.NewReader("{invalid json")) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec := httptest.NewRecorder() c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIUpdateClient_InvalidBody(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Orig", PublicKey: "pub1", AllocatedIPs: []string{"10.252.1.77/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req := httptest.NewRequest(http.MethodPut, "/api/v1/clients/"+id, strings.NewReader("{invalid")) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec := httptest.NewRecorder() c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIPatchClientStatus_InvalidBody(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Patch", PublicKey: "pub1", AllocatedIPs: []string{}, AllowedIPs: []string{}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req := httptest.NewRequest(http.MethodPatch, "/api/v1/clients/"+id+"/status", strings.NewReader("{invalid")) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec := httptest.NewRecorder() c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIPatchClientStatus(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIEmailClient_InvalidBody(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Bad Email Body", PublicKey: "pub1", AllocatedIPs: []string{"10.252.1.94/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) mailer := &mockEmailer{} req := httptest.NewRequest(http.MethodPost, "/api/v1/clients/"+id+"/email", strings.NewReader("{invalid")) req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) rec := httptest.NewRecorder() c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIEmailClient(env.db, mailer, "", "")(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } func TestAPIUpdateClient_InvalidAllocatedIPs(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Orig", PublicKey: "pub1", AllocatedIPs: []string{"10.252.1.76/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) body := map[string]interface{}{ "name": "Bad allocated", "allocated_ips": []string{"192.168.99.1/32"}, // outside server range "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": "pub1", } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) } // --- APICreateClient email validation --- func TestAPICreateClient_MissingEmail(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{ "name": "No Email", "allocated_ips": []string{"10.252.1.54/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "enabled": true, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "Email is required") } // --- APIListClients search and status filtering --- func TestAPIListClients_SearchFilter(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Alice Laptop", Email: "alice@test.com", AllocatedIPs: []string{"10.252.1.30/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Bob Phone", Email: "bob@test.com", AllocatedIPs: []string{"10.252.1.31/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: false, CreatedAt: now, UpdatedAt: now, }) // search by name req, rec := jsonRequest(http.MethodGet, "/api/v1/clients?search=alice", nil) c := env.echo.NewContext(req, rec) c.SetParamNames() c.QueryParams().Set("search", "alice") err := APIListClients(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var clients []model.ClientData parseJSON(t, rec, &clients) assert.Len(t, clients, 1) assert.Equal(t, "Alice Laptop", clients[0].Client.Name) } func TestAPIListClients_StatusFilter(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Enabled Client", Email: "en@test.com", AllocatedIPs: []string{"10.252.1.32/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Disabled Client", Email: "dis@test.com", AllocatedIPs: []string{"10.252.1.33/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: false, CreatedAt: now, UpdatedAt: now, }) // filter enabled only req, rec := jsonRequest(http.MethodGet, "/api/v1/clients?status=enabled", nil) c := env.echo.NewContext(req, rec) c.QueryParams().Set("status", "enabled") err := APIListClients(env.db)(c) require.NoError(t, err) var enabledClients []model.ClientData parseJSON(t, rec, &enabledClients) assert.Len(t, enabledClients, 1) assert.Equal(t, "Enabled Client", enabledClients[0].Client.Name) // filter disabled only req2, rec2 := jsonRequest(http.MethodGet, "/api/v1/clients?status=disabled", nil) c2 := env.echo.NewContext(req2, rec2) c2.QueryParams().Set("status", "disabled") err = APIListClients(env.db)(c2) require.NoError(t, err) var disabledClients []model.ClientData parseJSON(t, rec2, &disabledClients) assert.Len(t, disabledClients, 1) assert.Equal(t, "Disabled Client", disabledClients[0].Client.Name) } // --- APIExportClients --- func TestAPIExportClients(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Export Client", Email: "export@test.com", AllocatedIPs: []string{"10.252.1.40/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/export", nil) c := env.echo.NewContext(req, rec) err := APIExportClients(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) assert.Contains(t, rec.Header().Get("Content-Disposition"), "clients.xlsx") assert.Contains(t, rec.Header().Get("Content-Type"), "spreadsheetml.sheet") assert.True(t, rec.Body.Len() > 0) } // --- APIServerStatus --- // --- currentUserEmail Tests --- func TestCurrentUserEmail_DisabledLogin(t *testing.T) { env := setupTestEnv(t) util.DisableLogin = true req, rec := jsonRequest(http.MethodGet, "/test", nil) c := env.echo.NewContext(req, rec) email := currentUserEmail(c, env.db) // DisableLogin -> currentUser returns "" -> currentUserEmail returns "" assert.Equal(t, "", email) } func TestCurrentUserEmail_UserNotFound(t *testing.T) { origDisable := util.DisableLogin util.DisableLogin = false defer func() { util.DisableLogin = origDisable }() env := setupTestEnv(t) util.DisableLogin = false var email string env.echo.GET("/test-email-nf", func(c echo.Context) error { // session has a username that doesn't exist in DB createSession(c, "nonexistent", false, uint32(0), false) email = currentUserEmail(c, env.db) return c.String(http.StatusOK, "ok") }) req, rec := jsonRequest(http.MethodGet, "/test-email-nf", nil) env.echo.ServeHTTP(rec, req) assert.Equal(t, "", email) } func TestCurrentUserEmail_UserExists(t *testing.T) { origDisable := util.DisableLogin util.DisableLogin = false defer func() { util.DisableLogin = origDisable }() env := setupTestEnv(t) util.DisableLogin = false now := time.Now().UTC() env.db.SaveUser(model.User{Username: "emailuser", Email: "emailuser@test.com", Admin: false, CreatedAt: now, UpdatedAt: now}) var email string env.echo.GET("/test-email-found", func(c echo.Context) error { createSession(c, "emailuser", false, uint32(0), false) return c.String(http.StatusOK, "ok") }) env.echo.GET("/read-email", func(c echo.Context) error { email = currentUserEmail(c, env.db) return c.String(http.StatusOK, email) }) req1, rec1 := jsonRequest(http.MethodGet, "/test-email-found", nil) env.echo.ServeHTTP(rec1, req1) cookies := rec1.Result().Cookies() req2, rec2 := jsonRequest(http.MethodGet, "/read-email", nil) for _, cookie := range cookies { req2.AddCookie(cookie) } env.echo.ServeHTTP(rec2, req2) assert.Equal(t, "emailuser@test.com", email) } // --- APICreateClient with valid WireGuard public key --- func TestAPICreateClient_WithValidPublicKey(t *testing.T) { env := setupTestEnv(t) // Generate a real WireGuard key for testing key, err := wgtypes.GeneratePrivateKey() require.NoError(t, err) pubKey := key.PublicKey().String() body := map[string]interface{}{ "name": "External Key", "email": "extkey@test.com", "allocated_ips": []string{"10.252.1.55/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": pubKey, "enabled": true, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err = APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusCreated, rec.Code) var client model.Client parseJSON(t, rec, &client) assert.Equal(t, pubKey, client.PublicKey) assert.Empty(t, client.PrivateKey, "Private key should be empty when public key is provided") } // --- APICreateClient with valid preshared key --- func TestAPICreateClient_WithValidPresharedKey(t *testing.T) { env := setupTestEnv(t) psk, err := wgtypes.GenerateKey() require.NoError(t, err) body := map[string]interface{}{ "name": "PSK Client", "email": "psk-valid@test.com", "allocated_ips": []string{"10.252.1.56/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "preshared_key": psk.String(), "enabled": true, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err = APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusCreated, rec.Code) var client model.Client parseJSON(t, rec, &client) assert.Equal(t, psk.String(), client.PresharedKey) } // --- APICreateClient missing name --- func TestAPICreateClient_MissingName(t *testing.T) { env := setupTestEnv(t) body := map[string]interface{}{ "email": "noname@test.com", "allocated_ips": []string{"10.252.1.57/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "enabled": true, } req, rec := jsonRequest(http.MethodPost, "/api/v1/clients", body) c := env.echo.NewContext(req, rec) err := APICreateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "Name is required") } // --- APICreateClient duplicate name --- func TestAPICreateClient_DuplicateName(t *testing.T) { env := setupTestEnv(t) body1 := map[string]interface{}{ "name": "Unique Name", "email": "first@test.com", "allocated_ips": []string{"10.252.1.58/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "enabled": true, } req1, rec1 := jsonRequest(http.MethodPost, "/api/v1/clients", body1) c1 := env.echo.NewContext(req1, rec1) err := APICreateClient(env.db, env.cw)(c1) require.NoError(t, err) assert.Equal(t, http.StatusCreated, rec1.Code) body2 := map[string]interface{}{ "name": "unique name", // case-insensitive duplicate "email": "second@test.com", "allocated_ips": []string{"10.252.1.59/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "enabled": true, } req2, rec2 := jsonRequest(http.MethodPost, "/api/v1/clients", body2) c2 := env.echo.NewContext(req2, rec2) err = APICreateClient(env.db, env.cw)(c2) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec2.Code) assert.Contains(t, rec2.Body.String(), "name already exists") } // --- APICreateClient duplicate public key --- func TestAPICreateClient_DuplicatePublicKey(t *testing.T) { env := setupTestEnv(t) key, err := wgtypes.GeneratePrivateKey() require.NoError(t, err) pubKey := key.PublicKey().String() body1 := map[string]interface{}{ "name": "Client PKA", "email": "pka@test.com", "allocated_ips": []string{"10.252.1.61/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": pubKey, "enabled": true, } req1, rec1 := jsonRequest(http.MethodPost, "/api/v1/clients", body1) c1 := env.echo.NewContext(req1, rec1) err = APICreateClient(env.db, env.cw)(c1) require.NoError(t, err) assert.Equal(t, http.StatusCreated, rec1.Code) body2 := map[string]interface{}{ "name": "Client PKB", "email": "pkb@test.com", "allocated_ips": []string{"10.252.1.62/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": pubKey, // same key "enabled": true, } req2, rec2 := jsonRequest(http.MethodPost, "/api/v1/clients", body2) c2 := env.echo.NewContext(req2, rec2) err = APICreateClient(env.db, env.cw)(c2) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec2.Code) assert.Contains(t, rec2.Body.String(), "Duplicate public key") } // --- APIUpdateClient with valid public key change --- func TestAPIUpdateClient_ChangePublicKey_Valid(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() origKey, err := wgtypes.GeneratePrivateKey() require.NoError(t, err) env.db.SaveClient(model.Client{ ID: id, Name: "KeyChange", PublicKey: origKey.PublicKey().String(), PrivateKey: origKey.String(), AllocatedIPs: []string{"10.252.1.82/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) newKey, err := wgtypes.GeneratePrivateKey() require.NoError(t, err) newPubKey := newKey.PublicKey().String() body := map[string]interface{}{ "name": "KeyChange", "allocated_ips": []string{"10.252.1.82/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": newPubKey, "preshared_key": "", "enabled": true, } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err = APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var client model.Client parseJSON(t, rec, &client) assert.Equal(t, newPubKey, client.PublicKey) assert.Empty(t, client.PrivateKey, "Private key should be cleared when public key changes") } // --- APIUpdateClient with valid preshared key change --- func TestAPIUpdateClient_ChangePresharedKey_Valid(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() origPSK, err := wgtypes.GenerateKey() require.NoError(t, err) env.db.SaveClient(model.Client{ ID: id, Name: "PSKChange", PublicKey: "pubX", PresharedKey: origPSK.String(), AllocatedIPs: []string{"10.252.1.83/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) newPSK, err := wgtypes.GenerateKey() require.NoError(t, err) body := map[string]interface{}{ "name": "PSKChange", "allocated_ips": []string{"10.252.1.83/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": "pubX", "preshared_key": newPSK.String(), "enabled": true, } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err = APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var client model.Client parseJSON(t, rec, &client) assert.Equal(t, newPSK.String(), client.PresharedKey) } // --- APIUpdateClient duplicate name --- func TestAPIUpdateClient_DuplicateName(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() id1 := xid.New().String() id2 := xid.New().String() env.db.SaveClient(model.Client{ ID: id1, Name: "Client Alpha", PublicKey: "alpha-pub", AllocatedIPs: []string{"10.252.1.84/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: id2, Name: "Client Beta", PublicKey: "beta-pub", AllocatedIPs: []string{"10.252.1.85/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) // Try to rename Beta to Alpha body := map[string]interface{}{ "name": "Client Alpha", "allocated_ips": []string{"10.252.1.85/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": "beta-pub", "preshared_key": "", "enabled": true, } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id2, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id2) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "name already exists") } // --- APIUpdateClient duplicate public key --- func TestAPIUpdateClient_DuplicatePublicKey(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() key1, err := wgtypes.GeneratePrivateKey() require.NoError(t, err) key2, err := wgtypes.GeneratePrivateKey() require.NoError(t, err) id1 := xid.New().String() id2 := xid.New().String() env.db.SaveClient(model.Client{ ID: id1, Name: "DupPK A", PublicKey: key1.PublicKey().String(), AllocatedIPs: []string{"10.252.1.86/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: id2, Name: "DupPK B", PublicKey: key2.PublicKey().String(), AllocatedIPs: []string{"10.252.1.87/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) // Try to set B's public key to A's body := map[string]interface{}{ "name": "DupPK B", "allocated_ips": []string{"10.252.1.87/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": key1.PublicKey().String(), "preshared_key": "", "enabled": true, } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id2, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id2) err = APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "Duplicate public key") } // --- APIUpdateClient missing name --- func TestAPIUpdateClient_MissingName(t *testing.T) { env := setupTestEnv(t) id := xid.New().String() now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: id, Name: "Orig Name", PublicKey: "pubx", AllocatedIPs: []string{"10.252.1.88/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) body := map[string]interface{}{ "name": " ", "allocated_ips": []string{"10.252.1.88/32"}, "allowed_ips": []string{"0.0.0.0/0"}, "extra_allowed_ips": []string{}, "public_key": "pubx", } req, rec := jsonRequest(http.MethodPut, "/api/v1/clients/"+id, body) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIUpdateClient(env.db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "Name is required") } // --- APIListClients search by email --- func TestAPIListClients_SearchByEmail(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Client A", Email: "unique-email@test.com", AllocatedIPs: []string{"10.252.1.34/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Client B", Email: "other@test.com", AllocatedIPs: []string{"10.252.1.35/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients?search=unique-email", nil) c := env.echo.NewContext(req, rec) c.QueryParams().Set("search", "unique-email") err := APIListClients(env.db)(c) require.NoError(t, err) var clients []model.ClientData parseJSON(t, rec, &clients) assert.Len(t, clients, 1) assert.Equal(t, "Client A", clients[0].Client.Name) } // --- APIListClients search by IP --- func TestAPIListClients_SearchByIP(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "IP Client", Email: "ip@test.com", AllocatedIPs: []string{"10.252.1.36/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients?search=10.252.1.36", nil) c := env.echo.NewContext(req, rec) c.QueryParams().Set("search", "10.252.1.36") err := APIListClients(env.db)(c) require.NoError(t, err) var clients []model.ClientData parseJSON(t, rec, &clients) assert.Len(t, clients, 1) assert.Equal(t, "IP Client", clients[0].Client.Name) } // --- Non-admin access tests --- // Register ALL routes before the first ServeHTTP call to avoid Echo router panics. func TestNonAdmin_ClientAccess(t *testing.T) { origDisable := util.DisableLogin util.DisableLogin = false defer func() { util.DisableLogin = origDisable }() env := setupTestEnv(t) util.DisableLogin = false now := time.Now().UTC() env.db.SaveUser(model.User{Username: "naviewer", Email: "naviewer@test.com", Admin: false, CreatedAt: now, UpdatedAt: now}) crc := util.GetDBUserCRC32(model.User{Username: "naviewer", Email: "naviewer@test.com", Admin: false, CreatedAt: now, UpdatedAt: now}) util.DBUsersToCRC32Mutex.Lock() util.DBUsersToCRC32["naviewer"] = crc util.DBUsersToCRC32Mutex.Unlock() defer func() { util.DBUsersToCRC32Mutex.Lock() delete(util.DBUsersToCRC32, "naviewer") util.DBUsersToCRC32Mutex.Unlock() }() ownID := xid.New().String() otherID := xid.New().String() env.db.SaveClient(model.Client{ ID: ownID, Name: "My Own", Email: "naviewer@test.com", PublicKey: "myownpub", PrivateKey: "myownpriv", AllocatedIPs: []string{"10.252.1.110/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, UseServerDNS: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: otherID, Name: "Others", Email: "other@test.com", PublicKey: "otherspub", PrivateKey: "otherspriv", AllocatedIPs: []string{"10.252.1.111/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) // Register ALL routes before first ServeHTTP env.echo.GET("/na-setup", func(c echo.Context) error { createSession(c, "naviewer", false, crc, false) return c.String(http.StatusOK, "ok") }) env.echo.GET("/na-get/:id", APIGetClient(env.db)) env.echo.GET("/na-dl/:id", APIDownloadClientConfig(env.db)) env.echo.GET("/na-qr/:id", APIGetClientQRCode(env.db)) env.echo.GET("/na-list", APIListClients(env.db)) // Create session req1, rec1 := jsonRequest(http.MethodGet, "/na-setup", nil) env.echo.ServeHTTP(rec1, req1) require.Equal(t, http.StatusOK, rec1.Code) cookies := rec1.Result().Cookies() addCookies := func(req *http.Request) { for _, cookie := range cookies { req.AddCookie(cookie) } } // Test: get own client -> OK req, rec := jsonRequest(http.MethodGet, "/na-get/"+ownID, nil) addCookies(req) env.echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Non-admin should see own client") // Test: get other's client -> Forbidden req, rec = jsonRequest(http.MethodGet, "/na-get/"+otherID, nil) addCookies(req) env.echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code, "Non-admin should not see other's client") // Test: download own config -> OK req, rec = jsonRequest(http.MethodGet, "/na-dl/"+ownID, nil) addCookies(req) env.echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Non-admin should download own config") assert.Contains(t, rec.Body.String(), "[Interface]") // Test: download other's config -> Forbidden req, rec = jsonRequest(http.MethodGet, "/na-dl/"+otherID, nil) addCookies(req) env.echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code, "Non-admin should not download other's config") // Test: get own QR -> OK req, rec = jsonRequest(http.MethodGet, "/na-qr/"+ownID, nil) addCookies(req) env.echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code, "Non-admin should see own QR code") // Test: get other's QR -> Forbidden req, rec = jsonRequest(http.MethodGet, "/na-qr/"+otherID, nil) addCookies(req) env.echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code, "Non-admin should not see other's QR code") // Test: list clients -> only own req, rec = jsonRequest(http.MethodGet, "/na-list", nil) addCookies(req) env.echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusOK, rec.Code) var clients []model.ClientData parseJSON(t, rec, &clients) assert.Len(t, clients, 1, "Non-admin should only see own clients") assert.Equal(t, "My Own", clients[0].Client.Name) } // --- Non-admin APIEmailClient --- func TestAPIEmailClient_NonAdmin_OtherClient(t *testing.T) { origDisable := util.DisableLogin util.DisableLogin = false defer func() { util.DisableLogin = origDisable }() env := setupTestEnv(t) util.DisableLogin = false now := time.Now().UTC() env.db.SaveUser(model.User{Username: "emailna", Email: "emailna@test.com", Admin: false, CreatedAt: now, UpdatedAt: now}) crc := util.GetDBUserCRC32(model.User{Username: "emailna", Email: "emailna@test.com", Admin: false, CreatedAt: now, UpdatedAt: now}) util.DBUsersToCRC32Mutex.Lock() util.DBUsersToCRC32["emailna"] = crc util.DBUsersToCRC32Mutex.Unlock() defer func() { util.DBUsersToCRC32Mutex.Lock() delete(util.DBUsersToCRC32, "emailna") util.DBUsersToCRC32Mutex.Unlock() }() id := xid.New().String() env.db.SaveClient(model.Client{ ID: id, Name: "Email Other", Email: "other@test.com", PublicKey: "emailothpub", PrivateKey: "emailothpriv", AllocatedIPs: []string{"10.252.1.104/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) mailer := &mockEmailer{} // Register routes before any ServeHTTP env.echo.GET("/email-na-setup", func(c echo.Context) error { createSession(c, "emailna", false, crc, false) return c.String(http.StatusOK, "ok") }) env.echo.POST("/email-deny/:id", APIEmailClient(env.db, mailer, "Subject", "Body")) // Create session req1, rec1 := jsonRequest(http.MethodGet, "/email-na-setup", nil) env.echo.ServeHTTP(rec1, req1) require.Equal(t, http.StatusOK, rec1.Code) cookies := rec1.Result().Cookies() body := map[string]string{"email": "test@test.com"} req, rec := jsonRequest(http.MethodPost, "/email-deny/"+id, body) for _, cookie := range cookies { req.AddCookie(cookie) } env.echo.ServeHTTP(rec, req) assert.Equal(t, http.StatusForbidden, rec.Code) } // --- APISuggestClientIPs with subnet range parameter --- func TestAPISuggestClientIPs_WithSubnetRange(t *testing.T) { env := setupTestEnv(t) // Set up a subnet range within the server's address space origRanges := util.SubnetRanges origOrder := util.SubnetRangesOrder defer func() { util.SubnetRanges = origRanges util.SubnetRangesOrder = origOrder }() util.SubnetRanges = util.ParseSubnetRanges("testrange:10.252.1.0/26") req, rec := jsonRequest(http.MethodGet, "/api/v1/suggest-client-ips?sr=testrange", nil) c := env.echo.NewContext(req, rec) c.QueryParams().Set("sr", "testrange") err := APISuggestClientIPs(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var ips []string parseJSON(t, rec, &ips) assert.NotEmpty(t, ips) } func TestAPISuggestClientIPs_AllAllocated(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() // Allocate many IPs to exhaust the subnet // Server uses 10.252.1.0/24 by default. The server itself uses .1. // Let's allocate the first few IPs and check we still get a suggestion for i := 2; i < 5; i++ { env.db.SaveClient(model.Client{ ID: fmt.Sprintf("exhaust-%d", i), Name: fmt.Sprintf("Client %d", i), AllocatedIPs: []string{fmt.Sprintf("10.252.1.%d/32", i)}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) } req, rec := jsonRequest(http.MethodGet, "/api/v1/suggest-client-ips", nil) c := env.echo.NewContext(req, rec) err := APISuggestClientIPs(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var ips []string parseJSON(t, rec, &ips) assert.NotEmpty(t, ips) } // --- APIListClients search by name with no results --- func TestAPIListClients_SearchNoMatch(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Alice", Email: "alice@test.com", AllocatedIPs: []string{"10.252.1.120/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/clients?search=zzzznotfound", nil) c := env.echo.NewContext(req, rec) c.QueryParams().Set("search", "zzzznotfound") err := APIListClients(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var clients []model.ClientData parseJSON(t, rec, &clients) assert.Len(t, clients, 0) } // --- APIListClients with multiple clients and search by partial IP --- func TestAPIListClients_SearchByPartialIP(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "PartialIPClient1", Email: "pip1@test.com", AllocatedIPs: []string{"10.252.1.121/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "PartialIPClient2", Email: "pip2@test.com", AllocatedIPs: []string{"10.252.1.122/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "PartialIPClient3", Email: "pip3@test.com", AllocatedIPs: []string{"10.252.2.10/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) // Search with partial IP that matches two clients req, rec := jsonRequest(http.MethodGet, "/api/v1/clients?search=252.1.12", nil) c := env.echo.NewContext(req, rec) c.QueryParams().Set("search", "252.1.12") err := APIListClients(env.db)(c) require.NoError(t, err) var clients []model.ClientData parseJSON(t, rec, &clients) assert.Len(t, clients, 2) } // --- APISuggestClientIPs with unknown subnet range falls back to server addresses --- func TestAPISuggestClientIPs_UnknownSubnetRange(t *testing.T) { env := setupTestEnv(t) req, rec := jsonRequest(http.MethodGet, "/api/v1/suggest-client-ips?sr=nonexistent", nil) c := env.echo.NewContext(req, rec) c.QueryParams().Set("sr", "nonexistent") err := APISuggestClientIPs(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var ips []string parseJSON(t, rec, &ips) assert.NotEmpty(t, ips) // Falls back to server addresses } // --- APISuggestClientIPs with exhausted tiny subnet --- func TestAPISuggestClientIPs_ExhaustedSubnet(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() // Save custom server interface with a tiny /30 subnet (only 2 usable IPs) iface := model.ServerInterface{ Addresses: []string{"10.253.0.0/30"}, ListenPort: 51820, UpdatedAt: now, } require.NoError(t, env.db.SaveServerInterface(iface)) // Allocate all usable IPs (server takes .0, so .1 and .2 are available) env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Exhaust1", AllocatedIPs: []string{"10.253.0.1/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Exhaust2", AllocatedIPs: []string{"10.253.0.2/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/suggest-client-ips", nil) c := env.echo.NewContext(req, rec) err := APISuggestClientIPs(env.db)(c) require.NoError(t, err) // Should return error since all IPs are exhausted assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(t, rec.Body.String(), "No available IPs") } // --- APISuggestClientIPs with IPv6 server --- func TestAPISuggestClientIPs_IPv6(t *testing.T) { env := setupTestEnv(t) now := time.Now().UTC() // Set up a dual-stack server iface := model.ServerInterface{ Addresses: []string{"10.252.1.0/24", "fd00:abcd::1/64"}, ListenPort: 51820, UpdatedAt: now, } require.NoError(t, env.db.SaveServerInterface(iface)) req, rec := jsonRequest(http.MethodGet, "/api/v1/suggest-client-ips", nil) c := env.echo.NewContext(req, rec) err := APISuggestClientIPs(env.db)(c) require.NoError(t, err) assert.Equal(t, http.StatusOK, rec.Code) var ips []string parseJSON(t, rec, &ips) assert.NotEmpty(t, ips) // Should contain both IPv4 (/32) and IPv6 (/128) suggestions hasIPv4 := false hasIPv6 := false for _, ip := range ips { if strings.HasSuffix(ip, "/32") { hasIPv4 = true } if strings.HasSuffix(ip, "/128") { hasIPv6 = true } } assert.True(t, hasIPv4, "Should suggest IPv4 address") assert.True(t, hasIPv6, "Should suggest IPv6 address") } // --- Error path tests using errStore --- func TestAPIListClients_DBError(t *testing.T) { db := &errStore{} e := echo.New() req, rec := jsonRequest(http.MethodGet, "/api/v1/clients", nil) c := e.NewContext(req, rec) err := APIListClients(db)(c) require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, rec.Code) } func TestAPISuggestClientIPs_DBServerError(t *testing.T) { db := &errStore{} e := echo.New() req, rec := jsonRequest(http.MethodGet, "/api/v1/suggest-client-ips", nil) c := e.NewContext(req, rec) err := APISuggestClientIPs(db)(c) require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, rec.Code) } func TestAPIConfigStatus_DBError(t *testing.T) { db := &errStore{} e := echo.New() req, rec := jsonRequest(http.MethodGet, "/api/v1/server/config-status", nil) c := e.NewContext(req, rec) err := APIConfigStatus(db)(c) require.NoError(t, err) // ConfigStatus uses util.HashesChanged which handles DB errors internally assert.Equal(t, http.StatusOK, rec.Code) } func TestAPIExportClients_DBError(t *testing.T) { db := &errStore{} e := echo.New() req, rec := jsonRequest(http.MethodGet, "/api/v1/clients/export", nil) c := e.NewContext(req, rec) err := APIExportClients(db)(c) require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, rec.Code) } func TestAPIDeleteClient_DBError(t *testing.T) { db := &errStore{} env := setupTestEnv(t) id := xid.New().String() req, rec := jsonRequest(http.MethodDelete, "/api/v1/clients/"+id, nil) c := env.echo.NewContext(req, rec) c.SetParamNames("id") c.SetParamValues(id) err := APIDeleteClient(db, env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, rec.Code) } func TestAPIApplyServerConfig_Error(t *testing.T) { env := setupTestEnv(t) // Set config file path to impossible location to make ApplyNow fail gs, _ := env.db.GetGlobalSettings() gs.ConfigFilePath = "/dev/null/impossible/path/wg0.conf" env.db.SaveGlobalSettings(gs) req, rec := jsonRequest(http.MethodPost, "/api/v1/server/apply-config", nil) c := env.echo.NewContext(req, rec) err := APIApplyServerConfig(env.cw)(c) require.NoError(t, err) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(t, rec.Body.String(), "Cannot apply config") } func TestAPIServerStatus(t *testing.T) { env := setupTestEnv(t) // Add some clients to exercise the peer matching logic now := time.Now().UTC() env.db.SaveClient(model.Client{ ID: xid.New().String(), Name: "Status Client", PublicKey: "statuspub", PrivateKey: "statuspriv", AllocatedIPs: []string{"10.252.1.95/32"}, AllowedIPs: []string{"0.0.0.0/0"}, ExtraAllowedIPs: []string{}, SubnetRanges: []string{}, Enabled: true, CreatedAt: now, UpdatedAt: now, }) req, rec := jsonRequest(http.MethodGet, "/api/v1/status", nil) c := env.echo.NewContext(req, rec) err := APIServerStatus(env.db)(c) require.NoError(t, err) // On systems with WireGuard support, wgctrl.New() succeeds and returns 200 // On systems without, it returns 500 assert.Contains(t, []int{http.StatusOK, http.StatusInternalServerError}, rec.Code) }