Add comprehensive test suite

This commit is contained in:
2026-02-01 17:53:03 -05:00
parent c5279243c0
commit 63c3c10f2b
8 changed files with 1696 additions and 15 deletions

View File

@@ -0,0 +1,439 @@
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")
})
}
}