mirror of https://github.com/h44z/wg-portal.git
				
				
				
			* add webhook event for peer state change (#444) new event types: connect and disconnect example payload: ```json { "event": "connect", "entity": "peer", "identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", "payload": { "PeerId": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", "IsConnected": true, "IsPingable": false, "LastPing": null, "BytesReceived": 1860, "BytesTransmitted": 10824, "LastHandshake": "2025-06-26T23:04:33.325216659+02:00", "Endpoint": "10.55.66.77:33874", "LastSessionStart": "2025-06-26T22:50:40.10221606+02:00" } } ``` * add webhook docs (#444)
This commit is contained in:
		
							parent
							
								
									94785c10ec
								
							
						
					
					
						commit
						be29abd29a
					
				|  | @ -669,7 +669,7 @@ The webhook section allows you to configure a webhook that is called on certain | ||||||
| A JSON object is sent in a POST request to the webhook URL with the following structure: | A JSON object is sent in a POST request to the webhook URL with the following structure: | ||||||
| ```json | ```json | ||||||
| { | { | ||||||
|   "event": "peer_created", |   "event": "update", | ||||||
|   "entity": "peer", |   "entity": "peer", | ||||||
|   "identifier": "the-peer-identifier", |   "identifier": "the-peer-identifier", | ||||||
|   "payload": { |   "payload": { | ||||||
|  | @ -679,6 +679,8 @@ A JSON object is sent in a POST request to the webhook URL with the following st | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | Further details can be found in the [usage documentation](../usage/webhooks.md). | ||||||
|  | 
 | ||||||
| ### `url` | ### `url` | ||||||
| - **Default:** *(empty)* | - **Default:** *(empty)* | ||||||
| - **Description:** The POST endpoint to which the webhook is sent. The URL must be reachable from the WireGuard Portal server. If the URL is empty, the webhook is disabled. | - **Description:** The POST endpoint to which the webhook is sent. The URL must be reachable from the WireGuard Portal server. If the URL is empty, the webhook is disabled. | ||||||
|  |  | ||||||
|  | @ -0,0 +1,86 @@ | ||||||
|  | 
 | ||||||
|  | Webhooks allow WireGuard Portal to notify external services about events such as user creation, device changes, or configuration updates. This enables integration with other systems and automation workflows. | ||||||
|  | 
 | ||||||
|  | When webhooks are configured and a specified event occurs, WireGuard Portal sends an HTTP **POST** request to the configured webhook URL.  | ||||||
|  | The payload contains event-specific data in JSON format. | ||||||
|  | 
 | ||||||
|  | ## Configuration | ||||||
|  | 
 | ||||||
|  | All available configuration options for webhooks can be found in the [configuration overview](../configuration/overview.md#webhook). | ||||||
|  | 
 | ||||||
|  | A basic webhook configuration looks like this: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | webhook: | ||||||
|  |   url: https://your-service.example.com/webhook | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Security | ||||||
|  | 
 | ||||||
|  | Webhooks can be secured by using a shared secret. This secret is included in the `Authorization` header of the webhook request, allowing your service to verify the authenticity of the request. | ||||||
|  | You can set the shared secret in the webhook configuration: | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | webhook: | ||||||
|  |   url: https://your-service.example.com/webhook | ||||||
|  |   secret: "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | You should also make sure that your webhook endpoint is secured with HTTPS to prevent eavesdropping and tampering. | ||||||
|  | 
 | ||||||
|  | ## Available Events | ||||||
|  | 
 | ||||||
|  | WireGuard Portal supports various events that can trigger webhooks. The following events are available: | ||||||
|  | 
 | ||||||
|  | - `create`: Triggered when a new entity is created. | ||||||
|  | - `update`: Triggered when an existing entity is updated. | ||||||
|  | - `delete`: Triggered when an entity is deleted. | ||||||
|  | - `connect`: Triggered when a user connects to the VPN. | ||||||
|  | - `disconnect`: Triggered when a user disconnects from the VPN. | ||||||
|  | 
 | ||||||
|  | The following entity types can trigger webhooks: | ||||||
|  | 
 | ||||||
|  | - `user`: When a WireGuard Portal user is created, updated, or deleted. | ||||||
|  | - `peer`: When a peer is created, updated, or deleted. This entity can also trigger `connect` and `disconnect` events. | ||||||
|  | - `interface`: When a device is created, updated, or deleted. | ||||||
|  | 
 | ||||||
|  | ## Payload Structure | ||||||
|  | 
 | ||||||
|  | All webhook events send a JSON payload containing relevant data. The structure of the payload depends on the event type and entity involved. | ||||||
|  | A common shell structure for webhook payloads is as follows: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "event": "create", | ||||||
|  |   "entity": "user", | ||||||
|  |   "identifier": "the-user-identifier", | ||||||
|  |   "payload": { | ||||||
|  |     // The payload of the event, e.g. peer data. | ||||||
|  |     // Check the API documentation for the exact structure. | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### Example Payload | ||||||
|  | 
 | ||||||
|  | The following payload is an example of a webhook event when a peer connects to the VPN: | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   "event": "connect", | ||||||
|  |   "entity": "peer", | ||||||
|  |   "identifier": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", | ||||||
|  |   "payload": { | ||||||
|  |     "PeerId": "Fb5TaziAs1WrPBjC/MFbWsIelVXvi0hDKZ3YQM9wmU8=", | ||||||
|  |     "IsConnected": true, | ||||||
|  |     "IsPingable": false, | ||||||
|  |     "LastPing": null, | ||||||
|  |     "BytesReceived": 1860, | ||||||
|  |     "BytesTransmitted": 10824, | ||||||
|  |     "LastHandshake": "2025-06-26T23:04:33.325216659+02:00", | ||||||
|  |     "Endpoint": "10.55.66.77:33874", | ||||||
|  |     "LastSessionStart": "2025-06-26T22:50:40.10221606+02:00" | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | @ -133,5 +133,5 @@ func (m *MetricsServer) UpdatePeerMetrics(peer *domain.Peer, status domain.PeerS | ||||||
| 	} | 	} | ||||||
| 	m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived)) | 	m.peerReceivedBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesReceived)) | ||||||
| 	m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted)) | 	m.peerSendBytesTotal.WithLabelValues(labels...).Set(float64(status.BytesTransmitted)) | ||||||
| 	m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected())) | 	m.peerIsConnected.WithLabelValues(labels...).Set(internal.BoolToFloat64(status.IsConnected)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -198,7 +198,7 @@ func NewPeerStats(enabled bool, src []domain.PeerStatus) *PeerStats { | ||||||
| 
 | 
 | ||||||
| 	for _, srcStat := range src { | 	for _, srcStat := range src { | ||||||
| 		stats[string(srcStat.PeerId)] = PeerStatData{ | 		stats[string(srcStat.PeerId)] = PeerStatData{ | ||||||
| 			IsConnected:      srcStat.IsConnected(), | 			IsConnected:      srcStat.IsConnected, | ||||||
| 			IsPingable:       srcStat.IsPingable, | 			IsPingable:       srcStat.IsPingable, | ||||||
| 			LastPing:         srcStat.LastPing, | 			LastPing:         srcStat.LastPing, | ||||||
| 			BytesReceived:    srcStat.BytesReceived, | 			BytesReceived:    srcStat.BytesReceived, | ||||||
|  |  | ||||||
|  | @ -36,6 +36,7 @@ const TopicPeerDeleted = "peer:deleted" | ||||||
| const TopicPeerUpdated = "peer:updated" | const TopicPeerUpdated = "peer:updated" | ||||||
| const TopicPeerInterfaceUpdated = "peer:interface:updated" | const TopicPeerInterfaceUpdated = "peer:interface:updated" | ||||||
| const TopicPeerIdentifierUpdated = "peer:identifier:updated" | const TopicPeerIdentifierUpdated = "peer:identifier:updated" | ||||||
|  | const TopicPeerStateChanged = "peer:state:changed" | ||||||
| 
 | 
 | ||||||
| // endregion peer-events
 | // endregion peer-events
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -64,6 +64,7 @@ func (m Manager) connectToMessageBus() { | ||||||
| 	_ = m.bus.Subscribe(app.TopicPeerCreated, m.handlePeerCreateEvent) | 	_ = m.bus.Subscribe(app.TopicPeerCreated, m.handlePeerCreateEvent) | ||||||
| 	_ = m.bus.Subscribe(app.TopicPeerUpdated, m.handlePeerUpdateEvent) | 	_ = m.bus.Subscribe(app.TopicPeerUpdated, m.handlePeerUpdateEvent) | ||||||
| 	_ = m.bus.Subscribe(app.TopicPeerDeleted, m.handlePeerDeleteEvent) | 	_ = m.bus.Subscribe(app.TopicPeerDeleted, m.handlePeerDeleteEvent) | ||||||
|  | 	_ = m.bus.Subscribe(app.TopicPeerStateChanged, m.handlePeerStateChangeEvent) | ||||||
| 
 | 
 | ||||||
| 	_ = m.bus.Subscribe(app.TopicInterfaceCreated, m.handleInterfaceCreateEvent) | 	_ = m.bus.Subscribe(app.TopicInterfaceCreated, m.handleInterfaceCreateEvent) | ||||||
| 	_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceUpdateEvent) | 	_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceUpdateEvent) | ||||||
|  | @ -135,6 +136,14 @@ func (m Manager) handleInterfaceDeleteEvent(iface domain.Interface) { | ||||||
| 	m.handleGenericEvent(WebhookEventDelete, iface) | 	m.handleGenericEvent(WebhookEventDelete, iface) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (m Manager) handlePeerStateChangeEvent(peerStatus domain.PeerStatus) { | ||||||
|  | 	if peerStatus.IsConnected { | ||||||
|  | 		m.handleGenericEvent(WebhookEventConnect, peerStatus) | ||||||
|  | 	} else { | ||||||
|  | 		m.handleGenericEvent(WebhookEventDisconnect, peerStatus) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (m Manager) handleGenericEvent(action WebhookEvent, payload any) { | func (m Manager) handleGenericEvent(action WebhookEvent, payload any) { | ||||||
| 	eventData, err := m.createWebhookData(action, payload) | 	eventData, err := m.createWebhookData(action, payload) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -177,6 +186,9 @@ func (m Manager) createWebhookData(action WebhookEvent, payload any) (*WebhookDa | ||||||
| 	case domain.Interface: | 	case domain.Interface: | ||||||
| 		d.Entity = WebhookEntityInterface | 		d.Entity = WebhookEntityInterface | ||||||
| 		d.Identifier = string(v.Identifier) | 		d.Identifier = string(v.Identifier) | ||||||
|  | 	case domain.PeerStatus: | ||||||
|  | 		d.Entity = WebhookEntityPeer | ||||||
|  | 		d.Identifier = string(v.PeerId) | ||||||
| 	default: | 	default: | ||||||
| 		return nil, fmt.Errorf("unsupported payload type: %T", v) | 		return nil, fmt.Errorf("unsupported payload type: %T", v) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -42,7 +42,9 @@ const ( | ||||||
| type WebhookEvent = string | type WebhookEvent = string | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| 	WebhookEventCreate WebhookEvent = "create" | 	WebhookEventCreate     WebhookEvent = "create" | ||||||
| 	WebhookEventUpdate WebhookEvent = "update" | 	WebhookEventUpdate     WebhookEvent = "update" | ||||||
| 	WebhookEventDelete WebhookEvent = "delete" | 	WebhookEventDelete     WebhookEvent = "delete" | ||||||
|  | 	WebhookEventConnect    WebhookEvent = "connect" | ||||||
|  | 	WebhookEventDisconnect WebhookEvent = "disconnect" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -43,6 +43,8 @@ type StatisticsMetricsServer interface { | ||||||
| type StatisticsEventBus interface { | type StatisticsEventBus interface { | ||||||
| 	// Subscribe subscribes to a topic
 | 	// Subscribe subscribes to a topic
 | ||||||
| 	Subscribe(topic string, fn interface{}) error | 	Subscribe(topic string, fn interface{}) error | ||||||
|  | 	// Publish sends a message to the message bus.
 | ||||||
|  | 	Publish(topic string, args ...any) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type StatisticsCollector struct { | type StatisticsCollector struct { | ||||||
|  | @ -55,6 +57,8 @@ type StatisticsCollector struct { | ||||||
| 	db StatisticsDatabaseRepo | 	db StatisticsDatabaseRepo | ||||||
| 	wg StatisticsInterfaceController | 	wg StatisticsInterfaceController | ||||||
| 	ms StatisticsMetricsServer | 	ms StatisticsMetricsServer | ||||||
|  | 
 | ||||||
|  | 	peerChangeEvent chan domain.PeerIdentifier | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewStatisticsCollector creates a new statistics collector.
 | // NewStatisticsCollector creates a new statistics collector.
 | ||||||
|  | @ -171,8 +175,12 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) { | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
| 				for _, peer := range peers { | 				for _, peer := range peers { | ||||||
|  | 					var connectionStateChanged bool | ||||||
|  | 					var newPeerStatus domain.PeerStatus | ||||||
| 					err = c.db.UpdatePeerStatus(ctx, peer.Identifier, | 					err = c.db.UpdatePeerStatus(ctx, peer.Identifier, | ||||||
| 						func(p *domain.PeerStatus) (*domain.PeerStatus, error) { | 						func(p *domain.PeerStatus) (*domain.PeerStatus, error) { | ||||||
|  | 							wasConnected := p.IsConnected | ||||||
|  | 
 | ||||||
| 							var lastHandshake *time.Time | 							var lastHandshake *time.Time | ||||||
| 							if !peer.LastHandshake.IsZero() { | 							if !peer.LastHandshake.IsZero() { | ||||||
| 								lastHandshake = &peer.LastHandshake | 								lastHandshake = &peer.LastHandshake | ||||||
|  | @ -186,6 +194,12 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) { | ||||||
| 							p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
 | 							p.BytesTransmitted = peer.BytesDownload // store bytes that where received from the peer and sent by the server
 | ||||||
| 							p.Endpoint = peer.Endpoint | 							p.Endpoint = peer.Endpoint | ||||||
| 							p.LastHandshake = lastHandshake | 							p.LastHandshake = lastHandshake | ||||||
|  | 							p.CalcConnected() | ||||||
|  | 
 | ||||||
|  | 							if wasConnected != p.IsConnected { | ||||||
|  | 								connectionStateChanged = true | ||||||
|  | 								newPeerStatus = *p // store new status for event publishing
 | ||||||
|  | 							} | ||||||
| 
 | 
 | ||||||
| 							// Update prometheus metrics
 | 							// Update prometheus metrics
 | ||||||
| 							go c.updatePeerMetrics(ctx, *p) | 							go c.updatePeerMetrics(ctx, *p) | ||||||
|  | @ -197,6 +211,11 @@ func (c *StatisticsCollector) collectPeerData(ctx context.Context) { | ||||||
| 					} else { | 					} else { | ||||||
| 						slog.Debug("updated peer status", "peer", peer.Identifier) | 						slog.Debug("updated peer status", "peer", peer.Identifier) | ||||||
| 					} | 					} | ||||||
|  | 
 | ||||||
|  | 					if connectionStateChanged { | ||||||
|  | 						// publish event if connection state changed
 | ||||||
|  | 						c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus) | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  | @ -298,12 +317,17 @@ func (c *StatisticsCollector) enqueuePingChecks(ctx context.Context) { | ||||||
| func (c *StatisticsCollector) pingWorker(ctx context.Context) { | func (c *StatisticsCollector) pingWorker(ctx context.Context) { | ||||||
| 	defer c.pingWaitGroup.Done() | 	defer c.pingWaitGroup.Done() | ||||||
| 	for peer := range c.pingJobs { | 	for peer := range c.pingJobs { | ||||||
|  | 		var connectionStateChanged bool | ||||||
|  | 		var newPeerStatus domain.PeerStatus | ||||||
|  | 
 | ||||||
| 		peerPingable := c.isPeerPingable(ctx, peer) | 		peerPingable := c.isPeerPingable(ctx, peer) | ||||||
| 		slog.Debug("peer ping check completed", "peer", peer.Identifier, "pingable", peerPingable) | 		slog.Debug("peer ping check completed", "peer", peer.Identifier, "pingable", peerPingable) | ||||||
| 
 | 
 | ||||||
| 		now := time.Now() | 		now := time.Now() | ||||||
| 		err := c.db.UpdatePeerStatus(ctx, peer.Identifier, | 		err := c.db.UpdatePeerStatus(ctx, peer.Identifier, | ||||||
| 			func(p *domain.PeerStatus) (*domain.PeerStatus, error) { | 			func(p *domain.PeerStatus) (*domain.PeerStatus, error) { | ||||||
|  | 				wasConnected := p.IsConnected | ||||||
|  | 
 | ||||||
| 				if peerPingable { | 				if peerPingable { | ||||||
| 					p.IsPingable = true | 					p.IsPingable = true | ||||||
| 					p.LastPing = &now | 					p.LastPing = &now | ||||||
|  | @ -311,6 +335,13 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) { | ||||||
| 					p.IsPingable = false | 					p.IsPingable = false | ||||||
| 					p.LastPing = nil | 					p.LastPing = nil | ||||||
| 				} | 				} | ||||||
|  | 				p.UpdatedAt = time.Now() | ||||||
|  | 				p.CalcConnected() | ||||||
|  | 
 | ||||||
|  | 				if wasConnected != p.IsConnected { | ||||||
|  | 					connectionStateChanged = true | ||||||
|  | 					newPeerStatus = *p // store new status for event publishing
 | ||||||
|  | 				} | ||||||
| 
 | 
 | ||||||
| 				// Update prometheus metrics
 | 				// Update prometheus metrics
 | ||||||
| 				go c.updatePeerMetrics(ctx, *p) | 				go c.updatePeerMetrics(ctx, *p) | ||||||
|  | @ -322,6 +353,11 @@ func (c *StatisticsCollector) pingWorker(ctx context.Context) { | ||||||
| 		} else { | 		} else { | ||||||
| 			slog.Debug("updated peer ping status", "peer", peer.Identifier) | 			slog.Debug("updated peer ping status", "peer", peer.Identifier) | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		if connectionStateChanged { | ||||||
|  | 			// publish event if connection state changed
 | ||||||
|  | 			c.bus.Publish(app.TopicPeerStateChanged, newPeerStatus) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -3,21 +3,23 @@ package domain | ||||||
| import "time" | import "time" | ||||||
| 
 | 
 | ||||||
| type PeerStatus struct { | type PeerStatus struct { | ||||||
| 	PeerId    PeerIdentifier `gorm:"primaryKey;column:identifier"` | 	PeerId    PeerIdentifier `gorm:"primaryKey;column:identifier" json:"PeerId"` | ||||||
| 	UpdatedAt time.Time      `gorm:"column:updated_at"` | 	UpdatedAt time.Time      `gorm:"column:updated_at" json:"-"` | ||||||
| 
 | 
 | ||||||
| 	IsPingable bool       `gorm:"column:pingable"` | 	IsConnected bool `gorm:"column:connected" json:"IsConnected"` // indicates if the peer is connected based on the last handshake or ping
 | ||||||
| 	LastPing   *time.Time `gorm:"column:last_ping"` |  | ||||||
| 
 | 
 | ||||||
| 	BytesReceived    uint64 `gorm:"column:received"` | 	IsPingable bool       `gorm:"column:pingable" json:"IsPingable"` | ||||||
| 	BytesTransmitted uint64 `gorm:"column:transmitted"` | 	LastPing   *time.Time `gorm:"column:last_ping" json:"LastPing"` | ||||||
| 
 | 
 | ||||||
| 	LastHandshake    *time.Time `gorm:"column:last_handshake"` | 	BytesReceived    uint64 `gorm:"column:received" json:"BytesReceived"` | ||||||
| 	Endpoint         string     `gorm:"column:endpoint"` | 	BytesTransmitted uint64 `gorm:"column:transmitted" json:"BytesTransmitted"` | ||||||
| 	LastSessionStart *time.Time `gorm:"column:last_session_start"` | 
 | ||||||
|  | 	LastHandshake    *time.Time `gorm:"column:last_handshake" json:"LastHandshake"` | ||||||
|  | 	Endpoint         string     `gorm:"column:endpoint" json:"Endpoint"` | ||||||
|  | 	LastSessionStart *time.Time `gorm:"column:last_session_start" json:"LastSessionStart"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s PeerStatus) IsConnected() bool { | func (s *PeerStatus) CalcConnected() { | ||||||
| 	oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
 | 	oldestHandshakeTime := time.Now().Add(-2 * time.Minute) // if a handshake is older than 2 minutes, the peer is no longer connected
 | ||||||
| 
 | 
 | ||||||
| 	handshakeValid := false | 	handshakeValid := false | ||||||
|  | @ -25,7 +27,7 @@ func (s PeerStatus) IsConnected() bool { | ||||||
| 		handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime) | 		handshakeValid = !s.LastHandshake.Before(oldestHandshakeTime) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return s.IsPingable || handshakeValid | 	s.IsConnected = s.IsPingable || handshakeValid | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type InterfaceStatus struct { | type InterfaceStatus struct { | ||||||
|  |  | ||||||
|  | @ -66,8 +66,9 @@ func TestPeerStatus_IsConnected(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 	for _, tt := range tests { | 	for _, tt := range tests { | ||||||
| 		t.Run(tt.name, func(t *testing.T) { | 		t.Run(tt.name, func(t *testing.T) { | ||||||
| 			if got := tt.status.IsConnected(); got != tt.want { | 			tt.status.CalcConnected() | ||||||
| 				t.Errorf("IsConnected() = %v, want %v", got, tt.want) | 			if got := tt.status.IsConnected; got != tt.want { | ||||||
|  | 				t.Errorf("IsConnected = %v, want %v", got, tt.want) | ||||||
| 			} | 			} | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -82,6 +82,7 @@ nav: | ||||||
|           - General: documentation/usage/general.md |           - General: documentation/usage/general.md | ||||||
|           - LDAP: documentation/usage/ldap.md |           - LDAP: documentation/usage/ldap.md | ||||||
|           - Security: documentation/usage/security.md |           - Security: documentation/usage/security.md | ||||||
|  |           - Webhooks: documentation/usage/webhooks.md | ||||||
|           - REST API: documentation/rest-api/api-doc.md |           - REST API: documentation/rest-api/api-doc.md | ||||||
|       - Upgrade: documentation/upgrade/v1.md |       - Upgrade: documentation/upgrade/v1.md | ||||||
|       - Monitoring: documentation/monitoring/prometheus.md |       - Monitoring: documentation/monitoring/prometheus.md | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue