440 lines
11 KiB
Go
440 lines
11 KiB
Go
package integration
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.dws.rip/DWS/dyn/internal/config"
|
|
"git.dws.rip/DWS/dyn/internal/database"
|
|
"git.dws.rip/DWS/dyn/internal/dns"
|
|
"git.dws.rip/DWS/dyn/internal/handlers"
|
|
"git.dws.rip/DWS/dyn/internal/models"
|
|
"git.dws.rip/DWS/dyn/internal/testutil"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func setupIntegrationTest(t *testing.T) (*gin.Engine, *database.DB, *testutil.MockTechnitiumServer, func()) {
|
|
// Set Gin to test mode
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
// Create temp database
|
|
tmpDB := "/tmp/test_integration_" + time.Now().Format("20060102150405") + ".db"
|
|
db, err := database.New(tmpDB)
|
|
require.NoError(t, err)
|
|
|
|
// Create mock Technitium server
|
|
mockDNS := testutil.NewMockTechnitiumServer()
|
|
|
|
// Create config
|
|
cfg := &config.Config{
|
|
BaseDomain: "test.rip",
|
|
SpaceSubdomain: "space",
|
|
ServerPort: "8080",
|
|
RateLimitPerIP: 100,
|
|
RateLimitPerToken: 100,
|
|
}
|
|
|
|
// Create DNS client pointing to mock
|
|
dnsClient := dns.NewClient(
|
|
mockDNS.URL(),
|
|
mockDNS.Token,
|
|
mockDNS.Username,
|
|
mockDNS.Password,
|
|
)
|
|
|
|
// Setup Gin router
|
|
router := gin.New()
|
|
router.LoadHTMLGlob("../../web/templates/*")
|
|
|
|
webHandler := handlers.NewWebHandler(db, cfg)
|
|
dynHandler := handlers.NewDynDNSHandler(db, dnsClient, cfg)
|
|
|
|
router.GET("/", webHandler.Index)
|
|
|
|
api := router.Group("/api")
|
|
{
|
|
api.GET("/check", webHandler.CheckSubdomain)
|
|
api.POST("/claim", webHandler.ClaimSpace)
|
|
api.GET("/nic/update", dynHandler.Update)
|
|
}
|
|
|
|
cleanup := func() {
|
|
db.Close()
|
|
mockDNS.Close()
|
|
os.Remove(tmpDB)
|
|
}
|
|
|
|
return router, db, mockDNS, cleanup
|
|
}
|
|
|
|
func TestIntegration_ClaimSpace(t *testing.T) {
|
|
router, _, _, cleanup := setupIntegrationTest(t)
|
|
defer cleanup()
|
|
|
|
tests := []struct {
|
|
name string
|
|
subdomain string
|
|
wantStatus int
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "successful claim",
|
|
subdomain: "myhome",
|
|
wantStatus: http.StatusCreated,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "duplicate claim",
|
|
subdomain: "myhome",
|
|
wantStatus: http.StatusConflict,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "profane subdomain blocked",
|
|
subdomain: "fuck",
|
|
wantStatus: http.StatusBadRequest,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "reserved subdomain blocked",
|
|
subdomain: "dws",
|
|
wantStatus: http.StatusBadRequest,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "invalid format",
|
|
subdomain: "ab",
|
|
wantStatus: http.StatusBadRequest,
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
body, _ := json.Marshal(models.CreateSpaceRequest{
|
|
Subdomain: tt.subdomain,
|
|
})
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.wantStatus, w.Code)
|
|
|
|
if !tt.wantError {
|
|
var resp models.CreateSpaceResponse
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, resp.Token)
|
|
assert.Equal(t, tt.subdomain, resp.Subdomain)
|
|
assert.Equal(t, tt.subdomain+".space.test.rip", resp.FQDN)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntegration_CheckSubdomain(t *testing.T) {
|
|
router, db, _, cleanup := setupIntegrationTest(t)
|
|
defer cleanup()
|
|
|
|
// Create a space first
|
|
ctx := context.Background()
|
|
_, err := db.CreateSpace(ctx, "existing")
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
subdomain string
|
|
wantAvailable bool
|
|
wantReason string
|
|
}{
|
|
{
|
|
name: "available subdomain",
|
|
subdomain: "newspace",
|
|
wantAvailable: true,
|
|
wantReason: "",
|
|
},
|
|
{
|
|
name: "existing subdomain",
|
|
subdomain: "existing",
|
|
wantAvailable: false,
|
|
wantReason: "",
|
|
},
|
|
{
|
|
name: "profane subdomain",
|
|
subdomain: "shit",
|
|
wantAvailable: false,
|
|
wantReason: "inappropriate",
|
|
},
|
|
{
|
|
name: "reserved subdomain",
|
|
subdomain: "dubey",
|
|
wantAvailable: false,
|
|
wantReason: "reserved",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(http.MethodGet, "/api/check?subdomain="+tt.subdomain, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tt.wantAvailable, resp["available"])
|
|
if tt.wantReason != "" {
|
|
assert.Equal(t, tt.wantReason, resp["reason"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntegration_DynDNSUpdate(t *testing.T) {
|
|
router, db, mockDNS, cleanup := setupIntegrationTest(t)
|
|
defer cleanup()
|
|
|
|
// Create a space
|
|
ctx := context.Background()
|
|
space, err := db.CreateSpace(ctx, "myrouter")
|
|
require.NoError(t, err)
|
|
|
|
// Create basic auth header
|
|
auth := base64.StdEncoding.EncodeToString([]byte("none:" + space.Token))
|
|
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
myip string
|
|
authHeader string
|
|
wantStatus int
|
|
wantBody string
|
|
}{
|
|
{
|
|
name: "successful update with IP",
|
|
hostname: "myrouter.space.test.rip",
|
|
myip: "192.168.1.100",
|
|
authHeader: "Basic " + auth,
|
|
wantStatus: http.StatusOK,
|
|
wantBody: "good 192.168.1.100",
|
|
},
|
|
{
|
|
name: "same IP - no change",
|
|
hostname: "myrouter.space.test.rip",
|
|
myip: "192.168.1.100",
|
|
authHeader: "Basic " + auth,
|
|
wantStatus: http.StatusOK,
|
|
wantBody: "nochg 192.168.1.100",
|
|
},
|
|
{
|
|
name: "different IP",
|
|
hostname: "myrouter.space.test.rip",
|
|
myip: "10.0.0.50",
|
|
authHeader: "Basic " + auth,
|
|
wantStatus: http.StatusOK,
|
|
wantBody: "good 10.0.0.50",
|
|
},
|
|
{
|
|
name: "missing auth",
|
|
hostname: "myrouter.space.test.rip",
|
|
myip: "192.168.1.1",
|
|
authHeader: "",
|
|
wantStatus: http.StatusUnauthorized,
|
|
wantBody: "badauth",
|
|
},
|
|
{
|
|
name: "invalid token",
|
|
hostname: "myrouter.space.test.rip",
|
|
myip: "192.168.1.1",
|
|
authHeader: "Basic " + base64.StdEncoding.EncodeToString([]byte("none:invalid-token")),
|
|
wantStatus: http.StatusUnauthorized,
|
|
wantBody: "badauth",
|
|
},
|
|
{
|
|
name: "wrong hostname",
|
|
hostname: "wrong.space.test.rip",
|
|
myip: "192.168.1.1",
|
|
authHeader: "Basic " + auth,
|
|
wantStatus: http.StatusBadRequest,
|
|
wantBody: "nohost",
|
|
},
|
|
{
|
|
name: "missing hostname",
|
|
hostname: "",
|
|
myip: "192.168.1.1",
|
|
authHeader: "Basic " + auth,
|
|
wantStatus: http.StatusBadRequest,
|
|
wantBody: "nohost",
|
|
},
|
|
{
|
|
name: "invalid IP",
|
|
hostname: "myrouter.space.test.rip",
|
|
myip: "not-an-ip",
|
|
authHeader: "Basic " + auth,
|
|
wantStatus: http.StatusBadRequest,
|
|
wantBody: "dnserr",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
url := "/api/nic/update?hostname=" + tt.hostname
|
|
if tt.myip != "" {
|
|
url += "&myip=" + tt.myip
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, url, nil)
|
|
if tt.authHeader != "" {
|
|
req.Header.Set("Authorization", tt.authHeader)
|
|
}
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, tt.wantStatus, w.Code)
|
|
assert.Contains(t, w.Body.String(), tt.wantBody)
|
|
})
|
|
}
|
|
|
|
// Verify DNS records were created
|
|
records := mockDNS.GetRecords()
|
|
assert.Len(t, records, 2) // A record + wildcard record
|
|
}
|
|
|
|
func TestIntegration_FullWorkflow(t *testing.T) {
|
|
router, _, mockDNS, cleanup := setupIntegrationTest(t)
|
|
defer cleanup()
|
|
|
|
// Step 1: Check if subdomain is available
|
|
req := httptest.NewRequest(http.MethodGet, "/api/check?subdomain=myhome", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var checkResp map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &checkResp)
|
|
assert.True(t, checkResp["available"].(bool))
|
|
|
|
// Step 2: Claim the space
|
|
claimBody, _ := json.Marshal(models.CreateSpaceRequest{Subdomain: "myhome"})
|
|
req = httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(claimBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
var claimResp models.CreateSpaceResponse
|
|
json.Unmarshal(w.Body.Bytes(), &claimResp)
|
|
token := claimResp.Token
|
|
require.NotEmpty(t, token)
|
|
|
|
// Step 3: Update DNS via DynDNS protocol
|
|
auth := base64.StdEncoding.EncodeToString([]byte("none:" + token))
|
|
req = httptest.NewRequest(http.MethodGet, "/api/nic/update?hostname=myhome.space.test.rip&myip=1.2.3.4", nil)
|
|
req.Header.Set("Authorization", "Basic "+auth)
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Contains(t, w.Body.String(), "good 1.2.3.4")
|
|
|
|
// Step 4: Verify DNS records were created in mock server
|
|
records := mockDNS.GetRecords()
|
|
assert.Len(t, records, 2)
|
|
|
|
// Check for A record
|
|
aRecord, exists := records["myhome.space.test.rip:A"]
|
|
assert.True(t, exists)
|
|
assert.Equal(t, "1.2.3.4", aRecord.IPAddress)
|
|
|
|
// Check for wildcard record
|
|
wildcardRecord, exists := records["*.myhome.space.test.rip:A"]
|
|
assert.True(t, exists)
|
|
assert.Equal(t, "1.2.3.4", wildcardRecord.IPAddress)
|
|
|
|
// Step 5: Try to claim same subdomain (should fail)
|
|
req = httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(claimBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusConflict, w.Code)
|
|
}
|
|
|
|
func TestIntegration_ProfanityFiltering(t *testing.T) {
|
|
router, _, _, cleanup := setupIntegrationTest(t)
|
|
defer cleanup()
|
|
|
|
profaneSubdomains := []string{
|
|
"fuck",
|
|
"shit",
|
|
"ass",
|
|
"bitch",
|
|
}
|
|
|
|
for _, subdomain := range profaneSubdomains {
|
|
t.Run(subdomain, func(t *testing.T) {
|
|
body, _ := json.Marshal(models.CreateSpaceRequest{Subdomain: subdomain})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp map[string]string
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Contains(t, resp["error"], "inappropriate")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntegration_CustomFilter(t *testing.T) {
|
|
router, _, _, cleanup := setupIntegrationTest(t)
|
|
defer cleanup()
|
|
|
|
reservedSubdomains := []string{
|
|
"dws",
|
|
"dubey",
|
|
"tanishq",
|
|
"tdubey",
|
|
"dub3y",
|
|
"t4nishq",
|
|
"dwsengineering",
|
|
"dubeydns",
|
|
}
|
|
|
|
for _, subdomain := range reservedSubdomains {
|
|
t.Run(subdomain, func(t *testing.T) {
|
|
body, _ := json.Marshal(models.CreateSpaceRequest{Subdomain: subdomain})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/claim", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
var resp map[string]string
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.Contains(t, resp["error"], "reserved")
|
|
})
|
|
}
|
|
}
|