Skip to content

Commit 0efb1cc

Browse files
thomas-sickertaphansal123Copilotclaude[bot]domdomegg
authored
Add DNS Verification API (#220)
Co-authored-by: Ameya Phansalkar <[email protected]> Co-authored-by: Copilot <[email protected]> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: adam jones <[email protected]>
1 parent 544a663 commit 0efb1cc

14 files changed

Lines changed: 1110 additions & 49 deletions
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package v0
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNormalizeDomain(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
input string
11+
expected string
12+
}{
13+
{
14+
name: "simple domain",
15+
input: "example.com",
16+
expected: "example.com",
17+
},
18+
{
19+
name: "domain with subdomain",
20+
input: "api.example.com",
21+
expected: "api.example.com",
22+
},
23+
{
24+
name: "domain with https protocol",
25+
input: "https://example.com",
26+
expected: "example.com",
27+
},
28+
{
29+
name: "domain with http protocol",
30+
input: "http://example.com",
31+
expected: "example.com",
32+
},
33+
{
34+
name: "domain with path",
35+
input: "https://example.com/path/to/resource",
36+
expected: "example.com",
37+
},
38+
{
39+
name: "domain with query parameters",
40+
input: "https://example.com?param=value",
41+
expected: "example.com",
42+
},
43+
{
44+
name: "domain with port",
45+
input: "https://example.com:8080",
46+
expected: "example.com:8080",
47+
},
48+
{
49+
name: "mixed case domain",
50+
input: "EXAMPLE.COM",
51+
expected: "example.com",
52+
},
53+
{
54+
name: "domain with mixed case and protocol",
55+
input: "https://API.EXAMPLE.COM/path",
56+
expected: "api.example.com",
57+
},
58+
{
59+
name: "github.io domain",
60+
input: "username.github.io",
61+
expected: "username.github.io",
62+
},
63+
{
64+
name: "github.io with protocol and path",
65+
input: "https://username.github.io/project",
66+
expected: "username.github.io",
67+
},
68+
}
69+
70+
for _, tt := range tests {
71+
t.Run(tt.name, func(t *testing.T) {
72+
result, err := normalizeDomain(tt.input)
73+
if err != nil {
74+
t.Errorf("normalizeDomain(%q) returned unexpected error: %v", tt.input, err)
75+
return
76+
}
77+
if result != tt.expected {
78+
t.Errorf("normalizeDomain(%q) = %q, want %q", tt.input, result, tt.expected)
79+
}
80+
})
81+
}
82+
}
83+
84+
func TestNormalizeDomainErrors(t *testing.T) {
85+
errorTests := []struct {
86+
name string
87+
input string
88+
}{
89+
{
90+
name: "empty string",
91+
input: "",
92+
},
93+
{
94+
name: "whitespace only",
95+
input: " ",
96+
},
97+
{
98+
name: "malformed URL",
99+
input: "http://",
100+
},
101+
}
102+
103+
for _, tt := range errorTests {
104+
t.Run(tt.name, func(t *testing.T) {
105+
result, err := normalizeDomain(tt.input)
106+
if err == nil {
107+
t.Errorf("normalizeDomain(%q) = %q, expected error", tt.input, result)
108+
}
109+
})
110+
}
111+
}
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
package v0_test
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
"time"
10+
11+
v0 "github.com/modelcontextprotocol/registry/internal/api/handlers/v0"
12+
"github.com/modelcontextprotocol/registry/internal/model"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/mock"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
// MockRegistryServiceForDomainVerification is a mock implementation of the RegistryService interface for domain verification tests
19+
type MockRegistryServiceForDomainVerification struct {
20+
mock.Mock
21+
}
22+
23+
func (m *MockRegistryServiceForDomainVerification) List(cursor string, limit int) ([]model.Server, string, error) {
24+
args := m.Mock.Called(cursor, limit)
25+
return args.Get(0).([]model.Server), args.String(1), args.Error(2)
26+
}
27+
28+
func (m *MockRegistryServiceForDomainVerification) GetByID(id string) (*model.ServerDetail, error) {
29+
args := m.Mock.Called(id)
30+
return args.Get(0).(*model.ServerDetail), args.Error(1)
31+
}
32+
33+
func (m *MockRegistryServiceForDomainVerification) Publish(serverDetail *model.ServerDetail) error {
34+
args := m.Mock.Called(serverDetail)
35+
return args.Error(0)
36+
}
37+
38+
func (m *MockRegistryServiceForDomainVerification) ClaimDomain(domain string) (*model.VerificationToken, error) {
39+
args := m.Mock.Called(domain)
40+
return args.Get(0).(*model.VerificationToken), args.Error(1)
41+
}
42+
43+
func (m *MockRegistryServiceForDomainVerification) GetDomainVerificationStatus(domain string) (*model.VerificationTokens, error) {
44+
args := m.Mock.Called(domain)
45+
return args.Get(0).(*model.VerificationTokens), args.Error(1)
46+
}
47+
48+
func TestClaimDomainHandler(t *testing.T) {
49+
tests := []struct {
50+
name string
51+
method string
52+
requestBody interface{}
53+
setupMocks func(*MockRegistryServiceForDomainVerification)
54+
expectedStatus int
55+
checkResponse func(t *testing.T, response *v0.DomainClaimResponse)
56+
}{
57+
{
58+
name: "successful domain claim",
59+
method: http.MethodPost,
60+
requestBody: v0.DomainClaimRequest{
61+
Domain: "example.com",
62+
},
63+
setupMocks: func(registry *MockRegistryServiceForDomainVerification) {
64+
registry.On("ClaimDomain", "example.com").Return(&model.VerificationToken{
65+
Token: "test-token-123",
66+
CreatedAt: time.Now(),
67+
}, nil)
68+
},
69+
expectedStatus: http.StatusCreated,
70+
checkResponse: func(t *testing.T, response *v0.DomainClaimResponse) {
71+
assert.Equal(t, "example.com", response.Domain)
72+
assert.Equal(t, "example.com", response.NormalizedDomain)
73+
assert.Equal(t, "test-token-123", response.Token)
74+
assert.NotEmpty(t, response.CreatedAt)
75+
},
76+
},
77+
{
78+
name: "method not allowed",
79+
method: http.MethodGet,
80+
requestBody: nil,
81+
expectedStatus: http.StatusMethodNotAllowed,
82+
},
83+
{
84+
name: "missing domain",
85+
method: http.MethodPost,
86+
requestBody: v0.DomainClaimRequest{
87+
Domain: "",
88+
},
89+
expectedStatus: http.StatusBadRequest,
90+
},
91+
}
92+
93+
for _, tt := range tests {
94+
t.Run(tt.name, func(t *testing.T) {
95+
mockRegistry := new(MockRegistryServiceForDomainVerification)
96+
97+
if tt.setupMocks != nil {
98+
tt.setupMocks(mockRegistry)
99+
}
100+
101+
handler := v0.ClaimDomainHandler(mockRegistry)
102+
103+
var reqBody []byte
104+
if tt.requestBody != nil {
105+
var err error
106+
reqBody, err = json.Marshal(tt.requestBody)
107+
require.NoError(t, err)
108+
}
109+
110+
req := httptest.NewRequest(tt.method, "/v0/domains/claim", bytes.NewReader(reqBody))
111+
112+
w := httptest.NewRecorder()
113+
handler(w, req)
114+
115+
assert.Equal(t, tt.expectedStatus, w.Code)
116+
117+
if tt.checkResponse != nil && w.Code == http.StatusCreated {
118+
var response v0.DomainClaimResponse
119+
err := json.Unmarshal(w.Body.Bytes(), &response)
120+
require.NoError(t, err)
121+
tt.checkResponse(t, &response)
122+
}
123+
124+
mockRegistry.AssertExpectations(t)
125+
})
126+
}
127+
}
128+
129+
func TestGetDomainStatusHandler(t *testing.T) {
130+
tests := []struct {
131+
name string
132+
method string
133+
queryParam string
134+
setupMocks func(*MockRegistryServiceForDomainVerification)
135+
expectedStatus int
136+
checkResponse func(t *testing.T, response *v0.DomainStatusResponse)
137+
}{
138+
{
139+
name: "domain with verified token",
140+
method: http.MethodGet,
141+
queryParam: "domain=verified.com",
142+
setupMocks: func(registry *MockRegistryServiceForDomainVerification) {
143+
verifiedAt := time.Now()
144+
registry.On("GetDomainVerificationStatus", "verified.com").Return(&model.VerificationTokens{
145+
VerifiedToken: &model.VerificationToken{
146+
Token: "verified-token",
147+
CreatedAt: time.Now(),
148+
LastVerifiedAt: &verifiedAt,
149+
},
150+
PendingTokens: []model.VerificationToken{},
151+
}, nil)
152+
},
153+
expectedStatus: http.StatusOK,
154+
checkResponse: func(t *testing.T, response *v0.DomainStatusResponse) {
155+
assert.Equal(t, "verified.com", response.Domain)
156+
assert.Equal(t, "verified", response.Status)
157+
},
158+
},
159+
{
160+
name: "domain with pending tokens only",
161+
method: http.MethodGet,
162+
queryParam: "domain=pending.com",
163+
setupMocks: func(registry *MockRegistryServiceForDomainVerification) {
164+
registry.On("GetDomainVerificationStatus", "pending.com").Return(&model.VerificationTokens{
165+
VerifiedToken: nil,
166+
PendingTokens: []model.VerificationToken{
167+
{
168+
Token: "pending-token-1",
169+
CreatedAt: time.Now(),
170+
},
171+
{
172+
Token: "pending-token-2",
173+
CreatedAt: time.Now(),
174+
},
175+
},
176+
}, nil)
177+
},
178+
expectedStatus: http.StatusOK,
179+
checkResponse: func(t *testing.T, response *v0.DomainStatusResponse) {
180+
assert.Equal(t, "pending.com", response.Domain)
181+
assert.Equal(t, "unverified", response.Status)
182+
},
183+
},
184+
{
185+
name: "method not allowed",
186+
method: http.MethodPost,
187+
queryParam: "",
188+
expectedStatus: http.StatusMethodNotAllowed,
189+
},
190+
{
191+
name: "missing domain parameter",
192+
method: http.MethodGet,
193+
queryParam: "",
194+
expectedStatus: http.StatusBadRequest,
195+
},
196+
}
197+
198+
for _, tt := range tests {
199+
t.Run(tt.name, func(t *testing.T) {
200+
mockRegistry := new(MockRegistryServiceForDomainVerification)
201+
202+
if tt.setupMocks != nil {
203+
tt.setupMocks(mockRegistry)
204+
}
205+
206+
handler := v0.GetDomainStatusHandler(mockRegistry)
207+
208+
url := "/v0/domains/status"
209+
if tt.queryParam != "" {
210+
url = url + "?" + tt.queryParam
211+
}
212+
213+
req := httptest.NewRequest(tt.method, url, nil)
214+
215+
w := httptest.NewRecorder()
216+
handler(w, req)
217+
218+
assert.Equal(t, tt.expectedStatus, w.Code)
219+
220+
if tt.checkResponse != nil && w.Code == http.StatusOK {
221+
var response v0.DomainStatusResponse
222+
err := json.Unmarshal(w.Body.Bytes(), &response)
223+
require.NoError(t, err)
224+
tt.checkResponse(t, &response)
225+
}
226+
227+
mockRegistry.AssertExpectations(t)
228+
})
229+
}
230+
}

internal/api/handlers/v0/publish_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ func (m *MockRegistryService) Publish(serverDetail *model.ServerDetail) error {
3737
return args.Error(0)
3838
}
3939

40+
func (m *MockRegistryService) ClaimDomain(domain string) (*model.VerificationToken, error) {
41+
args := m.Mock.Called(domain)
42+
return args.Get(0).(*model.VerificationToken), args.Error(1)
43+
}
44+
45+
func (m *MockRegistryService) GetDomainVerificationStatus(domain string) (*model.VerificationTokens, error) {
46+
args := m.Mock.Called(domain)
47+
return args.Get(0).(*model.VerificationTokens), args.Error(1)
48+
}
49+
4050
// MockAuthService is a mock implementation of the auth.Service interface
4151
type MockAuthService struct {
4252
mock.Mock

0 commit comments

Comments
 (0)