mirror of
https://github.com/lukaszraczylo/lolcathost.git
synced 2026-06-08 23:43:39 +00:00
Initial commit.
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
// Package protocol defines shared message types for client-daemon communication.
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SocketPath is the Unix socket path for daemon communication.
|
||||
const SocketPath = "/var/run/lolcathost.sock"
|
||||
|
||||
// RequestType defines the type of request.
|
||||
type RequestType string
|
||||
|
||||
const (
|
||||
RequestPing RequestType = "ping"
|
||||
RequestStatus RequestType = "status"
|
||||
RequestList RequestType = "list"
|
||||
RequestSet RequestType = "set"
|
||||
RequestAdd RequestType = "add"
|
||||
RequestDelete RequestType = "delete"
|
||||
RequestSync RequestType = "sync"
|
||||
RequestPreset RequestType = "preset"
|
||||
RequestRollback RequestType = "rollback"
|
||||
RequestBackups RequestType = "backups"
|
||||
RequestAddGroup RequestType = "add_group"
|
||||
RequestDeleteGroup RequestType = "delete_group"
|
||||
RequestRenameGroup RequestType = "rename_group"
|
||||
RequestListGroups RequestType = "list_groups"
|
||||
RequestAddPreset RequestType = "add_preset"
|
||||
RequestDeletePreset RequestType = "delete_preset"
|
||||
RequestListPresets RequestType = "list_presets"
|
||||
)
|
||||
|
||||
// ErrorCode defines standard error codes.
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrCodeInvalidRequest ErrorCode = "INVALID_REQUEST"
|
||||
ErrCodeInvalidDomain ErrorCode = "INVALID_DOMAIN"
|
||||
ErrCodeInvalidIP ErrorCode = "INVALID_IP"
|
||||
ErrCodeBlockedDomain ErrorCode = "BLOCKED_DOMAIN"
|
||||
ErrCodeRateLimited ErrorCode = "RATE_LIMITED"
|
||||
ErrCodeUnauthorized ErrorCode = "UNAUTHORIZED"
|
||||
ErrCodeNotFound ErrorCode = "NOT_FOUND"
|
||||
ErrCodeConflict ErrorCode = "CONFLICT"
|
||||
ErrCodeInternalError ErrorCode = "INTERNAL_ERROR"
|
||||
ErrCodePermissionError ErrorCode = "PERMISSION_ERROR"
|
||||
)
|
||||
|
||||
// Request represents a client request to the daemon.
|
||||
type Request struct {
|
||||
Type RequestType `json:"type"`
|
||||
Payload json.RawMessage `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// SetPayload is the payload for set requests.
|
||||
type SetPayload struct {
|
||||
Alias string `json:"alias"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Force bool `json:"force,omitempty"`
|
||||
}
|
||||
|
||||
// PresetPayload is the payload for preset requests.
|
||||
type PresetPayload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// RollbackPayload is the payload for rollback requests.
|
||||
type RollbackPayload struct {
|
||||
BackupName string `json:"backup_name"`
|
||||
}
|
||||
|
||||
// AddPayload is the payload for add requests.
|
||||
type AddPayload struct {
|
||||
Domain string `json:"domain"`
|
||||
IP string `json:"ip"`
|
||||
Alias string `json:"alias"`
|
||||
Group string `json:"group"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// DeletePayload is the payload for delete requests.
|
||||
type DeletePayload struct {
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
|
||||
// GroupPayload is the payload for group add/delete requests.
|
||||
type GroupPayload struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// RenameGroupPayload is the payload for rename_group requests.
|
||||
type RenameGroupPayload struct {
|
||||
OldName string `json:"old_name"`
|
||||
NewName string `json:"new_name"`
|
||||
}
|
||||
|
||||
// GroupsData is the data for list_groups responses.
|
||||
type GroupsData struct {
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
// AddPresetPayload is the payload for add_preset requests.
|
||||
type AddPresetPayload struct {
|
||||
Name string `json:"name"`
|
||||
Enable []string `json:"enable"`
|
||||
Disable []string `json:"disable"`
|
||||
}
|
||||
|
||||
// PresetInfo represents a preset with its configuration.
|
||||
type PresetInfo struct {
|
||||
Name string `json:"name"`
|
||||
Enable []string `json:"enable"`
|
||||
Disable []string `json:"disable"`
|
||||
}
|
||||
|
||||
// PresetsData is the data for list_presets responses.
|
||||
type PresetsData struct {
|
||||
Presets []PresetInfo `json:"presets"`
|
||||
}
|
||||
|
||||
// Response represents a daemon response.
|
||||
type Response struct {
|
||||
Status string `json:"status"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Code ErrorCode `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// StatusData is the data for status responses.
|
||||
type StatusData struct {
|
||||
Running bool `json:"running"`
|
||||
Version string `json:"version"`
|
||||
Uptime int64 `json:"uptime_seconds"`
|
||||
ActiveCount int `json:"active_count"`
|
||||
RequestCount int64 `json:"request_count"`
|
||||
}
|
||||
|
||||
// HostEntry represents a single host entry.
|
||||
type HostEntry struct {
|
||||
Domain string `json:"domain"`
|
||||
IP string `json:"ip"`
|
||||
Alias string `json:"alias"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
|
||||
// ListData is the data for list responses.
|
||||
type ListData struct {
|
||||
Entries []HostEntry `json:"entries"`
|
||||
}
|
||||
|
||||
// SetData is the data for set responses.
|
||||
type SetData struct {
|
||||
Domain string `json:"domain"`
|
||||
Applied bool `json:"applied"`
|
||||
}
|
||||
|
||||
// BackupsData is the data for backups responses.
|
||||
type BackupsData struct {
|
||||
Backups []BackupInfo `json:"backups"`
|
||||
}
|
||||
|
||||
// BackupInfo represents a backup file.
|
||||
type BackupInfo struct {
|
||||
Name string `json:"name"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// NewRequest creates a new request with the given type and payload.
|
||||
func NewRequest(reqType RequestType, payload interface{}) (*Request, error) {
|
||||
req := &Request{Type: reqType}
|
||||
if payload != nil {
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal payload: %w", err)
|
||||
}
|
||||
req.Payload = data
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewOKResponse creates a success response with optional data.
|
||||
func NewOKResponse(data interface{}) (*Response, error) {
|
||||
resp := &Response{Status: "ok"}
|
||||
if data != nil {
|
||||
dataBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal data: %w", err)
|
||||
}
|
||||
resp.Data = dataBytes
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// NewErrorResponse creates an error response.
|
||||
func NewErrorResponse(code ErrorCode, message string) *Response {
|
||||
return &Response{
|
||||
Status: "error",
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// ParsePayload unmarshals the request payload into the given target.
|
||||
func (r *Request) ParsePayload(target interface{}) error {
|
||||
if r.Payload == nil {
|
||||
return fmt.Errorf("no payload in request")
|
||||
}
|
||||
return json.Unmarshal(r.Payload, target)
|
||||
}
|
||||
|
||||
// ParseData unmarshals the response data into the given target.
|
||||
func (r *Response) ParseData(target interface{}) error {
|
||||
if r.Data == nil {
|
||||
return fmt.Errorf("no data in response")
|
||||
}
|
||||
return json.Unmarshal(r.Data, target)
|
||||
}
|
||||
|
||||
// IsOK returns true if the response indicates success.
|
||||
func (r *Response) IsOK() bool {
|
||||
return r.Status == "ok"
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewRequest(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
reqType RequestType
|
||||
payload interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ping request without payload",
|
||||
reqType: RequestPing,
|
||||
payload: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "set request with payload",
|
||||
reqType: RequestSet,
|
||||
payload: SetPayload{Alias: "test", Enabled: true},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "preset request with payload",
|
||||
reqType: RequestPreset,
|
||||
payload: PresetPayload{Name: "local"},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, err := NewRequest(tt.reqType, tt.payload)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.reqType, req.Type)
|
||||
if tt.payload != nil {
|
||||
assert.NotNil(t, req.Payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequest_ParsePayload(t *testing.T) {
|
||||
t.Run("valid payload", func(t *testing.T) {
|
||||
payload := SetPayload{Alias: "test-alias", Enabled: true, Force: false}
|
||||
req, err := NewRequest(RequestSet, payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
var parsed SetPayload
|
||||
err = req.ParsePayload(&parsed)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "test-alias", parsed.Alias)
|
||||
assert.True(t, parsed.Enabled)
|
||||
assert.False(t, parsed.Force)
|
||||
})
|
||||
|
||||
t.Run("nil payload", func(t *testing.T) {
|
||||
req := &Request{Type: RequestPing}
|
||||
var parsed SetPayload
|
||||
err := req.ParsePayload(&parsed)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewOKResponse(t *testing.T) {
|
||||
t.Run("with data", func(t *testing.T) {
|
||||
data := StatusData{
|
||||
Running: true,
|
||||
Version: "1.0.0",
|
||||
Uptime: 3600,
|
||||
ActiveCount: 5,
|
||||
RequestCount: 100,
|
||||
}
|
||||
|
||||
resp, err := NewOKResponse(data)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ok", resp.Status)
|
||||
assert.NotNil(t, resp.Data)
|
||||
assert.True(t, resp.IsOK())
|
||||
})
|
||||
|
||||
t.Run("without data", func(t *testing.T) {
|
||||
resp, err := NewOKResponse(nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ok", resp.Status)
|
||||
assert.Nil(t, resp.Data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewErrorResponse(t *testing.T) {
|
||||
resp := NewErrorResponse(ErrCodeBlockedDomain, "domain is blocked")
|
||||
|
||||
assert.Equal(t, "error", resp.Status)
|
||||
assert.Equal(t, ErrCodeBlockedDomain, resp.Code)
|
||||
assert.Equal(t, "domain is blocked", resp.Message)
|
||||
assert.False(t, resp.IsOK())
|
||||
}
|
||||
|
||||
func TestResponse_ParseData(t *testing.T) {
|
||||
t.Run("valid data", func(t *testing.T) {
|
||||
data := ListData{
|
||||
Entries: []HostEntry{
|
||||
{Domain: "example.com", IP: "127.0.0.1", Alias: "example", Enabled: true, Group: "dev"},
|
||||
},
|
||||
}
|
||||
resp, err := NewOKResponse(data)
|
||||
require.NoError(t, err)
|
||||
|
||||
var parsed ListData
|
||||
err = resp.ParseData(&parsed)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, parsed.Entries, 1)
|
||||
assert.Equal(t, "example.com", parsed.Entries[0].Domain)
|
||||
})
|
||||
|
||||
t.Run("nil data", func(t *testing.T) {
|
||||
resp := &Response{Status: "ok"}
|
||||
var parsed ListData
|
||||
err := resp.ParseData(&parsed)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRequestTypes(t *testing.T) {
|
||||
types := []RequestType{
|
||||
RequestPing,
|
||||
RequestStatus,
|
||||
RequestList,
|
||||
RequestSet,
|
||||
RequestSync,
|
||||
RequestPreset,
|
||||
RequestRollback,
|
||||
RequestBackups,
|
||||
}
|
||||
|
||||
for _, rt := range types {
|
||||
t.Run(string(rt), func(t *testing.T) {
|
||||
req, err := NewRequest(rt, nil)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, rt, req.Type)
|
||||
|
||||
// Verify JSON marshaling works
|
||||
data, err := json.Marshal(req)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(data), string(rt))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorCodes(t *testing.T) {
|
||||
codes := []ErrorCode{
|
||||
ErrCodeInvalidRequest,
|
||||
ErrCodeInvalidDomain,
|
||||
ErrCodeInvalidIP,
|
||||
ErrCodeBlockedDomain,
|
||||
ErrCodeRateLimited,
|
||||
ErrCodeNotFound,
|
||||
ErrCodeConflict,
|
||||
ErrCodeInternalError,
|
||||
ErrCodePermissionError,
|
||||
}
|
||||
|
||||
for _, code := range codes {
|
||||
t.Run(string(code), func(t *testing.T) {
|
||||
resp := NewErrorResponse(code, "test error")
|
||||
assert.Equal(t, code, resp.Code)
|
||||
|
||||
// Verify JSON marshaling works
|
||||
data, err := json.Marshal(resp)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(data), string(code))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostEntry(t *testing.T) {
|
||||
entry := HostEntry{
|
||||
Domain: "example.com",
|
||||
IP: "127.0.0.1",
|
||||
Alias: "example-local",
|
||||
Enabled: true,
|
||||
Group: "development",
|
||||
}
|
||||
|
||||
data, err := json.Marshal(entry)
|
||||
require.NoError(t, err)
|
||||
|
||||
var parsed HostEntry
|
||||
err = json.Unmarshal(data, &parsed)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, entry.Domain, parsed.Domain)
|
||||
assert.Equal(t, entry.IP, parsed.IP)
|
||||
assert.Equal(t, entry.Alias, parsed.Alias)
|
||||
assert.Equal(t, entry.Enabled, parsed.Enabled)
|
||||
assert.Equal(t, entry.Group, parsed.Group)
|
||||
}
|
||||
|
||||
func TestBackupInfo(t *testing.T) {
|
||||
info := BackupInfo{
|
||||
Name: "hosts.20231201-120000.bak",
|
||||
Timestamp: 1701432000,
|
||||
Size: 1024,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(info)
|
||||
require.NoError(t, err)
|
||||
|
||||
var parsed BackupInfo
|
||||
err = json.Unmarshal(data, &parsed)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, info.Name, parsed.Name)
|
||||
assert.Equal(t, info.Timestamp, parsed.Timestamp)
|
||||
assert.Equal(t, info.Size, parsed.Size)
|
||||
}
|
||||
Reference in New Issue
Block a user