test(storage): 使用进程内 DNS 夹具消除离线测试依赖 (#689)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/outbound/outboundtest"
|
||||
"infinite-canvas/backend/internal/repository"
|
||||
|
||||
"gorm.io/driver/sqlite"
|
||||
@@ -470,6 +471,7 @@ func TestNormalizeAdminChannelModelDeleteIDsRequiresBoundedSelection(t *testing.
|
||||
}
|
||||
|
||||
func TestResolveProviderConfigMapsSKUToProviderModel(t *testing.T) {
|
||||
outboundtest.PublicDNS(t, "ark.cn-beijing.volces.com")
|
||||
svc, db := newChannelModelTestService(t)
|
||||
svc.dataDir = t.TempDir()
|
||||
channel := model.ModelChannel{
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"infinite-canvas/backend/internal/assets"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/outbound/outboundtest"
|
||||
"infinite-canvas/backend/internal/repository"
|
||||
"infinite-canvas/backend/internal/storage"
|
||||
|
||||
@@ -335,6 +336,7 @@ func TestAliyunOSSSettingKeepsCDNBaseURL(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQiniuKodoSettingAllowsMissingCDNBaseURL(t *testing.T) {
|
||||
outboundtest.PublicDNS(t, "up-z0.qiniup.com")
|
||||
next, err := ossSettingFromRequest(OSSSettingRequest{
|
||||
Enabled: true, Provider: qiniuKodoProvider, Region: "z0", Endpoint: "https://up-z0.qiniup.com",
|
||||
Bucket: "private-bucket", AccessKeyID: "access-id", AccessKeySecret: "secret-value",
|
||||
@@ -415,6 +417,7 @@ func TestArchivedProviderCredentialsAreEncryptedAtRest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResourceAccessChecksOwnershipAndSignsOSSResource(t *testing.T) {
|
||||
outboundtest.PublicDNS(t, "s3.amazonaws.com")
|
||||
svc := newResourceTestService(t)
|
||||
settingJSON, _ := json.Marshal(ossSettingValue{
|
||||
Enabled: true, Provider: "aliyun", Endpoint: "https://s3.amazonaws.com", Bucket: "private-bucket",
|
||||
@@ -472,6 +475,7 @@ func TestPrepareResourceDeliveryPrefersConfiguredCDN(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrepareResourceDeliveryFallsBackToOriginWhenCDNAuthIsMissing(t *testing.T) {
|
||||
outboundtest.PublicDNS(t, "s3.amazonaws.com")
|
||||
svc := newResourceTestService(t)
|
||||
settingJSON, _ := json.Marshal(ossSettingValue{
|
||||
Enabled: true, Provider: aliyunOSSProvider, Endpoint: "https://s3.amazonaws.com", CDNBaseURL: "https://media.example.com",
|
||||
@@ -687,6 +691,7 @@ func TestHistoricalUserResourceWithoutStorageSettingIDKeepsItsProviderCDN(t *tes
|
||||
}
|
||||
|
||||
func TestPrepareResourceDeliveryUsesSignedOriginWithoutCDN(t *testing.T) {
|
||||
outboundtest.PublicDNS(t, "s3.amazonaws.com")
|
||||
svc := newResourceTestService(t)
|
||||
settingJSON, _ := json.Marshal(ossSettingValue{
|
||||
Enabled: true, Provider: aliyunOSSProvider, Endpoint: "https://s3.amazonaws.com", Bucket: "private-bucket",
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"infinite-canvas/backend/internal/kernel"
|
||||
"infinite-canvas/backend/internal/model"
|
||||
"infinite-canvas/backend/internal/outbound/outboundtest"
|
||||
"infinite-canvas/backend/internal/storage"
|
||||
)
|
||||
|
||||
@@ -26,6 +27,7 @@ func testPlatformURL(variants *[]ResourceVariant) func(ResourceVariant, time.Tim
|
||||
}
|
||||
|
||||
func TestResolveAccessPolicyMatrix(t *testing.T) {
|
||||
outboundtest.PublicDNS(t, "s3.amazonaws.com")
|
||||
now := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -55,7 +57,7 @@ func TestResolveAccessPolicyMatrix(t *testing.T) {
|
||||
{
|
||||
name: "CDN without supported auth falls back to public origin",
|
||||
resource: testReadyResource("aliyun"),
|
||||
setting: storage.Settings{Provider: "aliyun", Endpoint: "https://s3.amazonaws.com", CDNBaseURL: "https://media.example.com", AccessKeyID: "id", AccessKeySecret: "secret"},
|
||||
setting: storage.Settings{Provider: "aliyun", Endpoint: "https://s3.amazonaws.com", Bucket: "private-bucket", CDNBaseURL: "https://media.example.com", AccessKeyID: "id", AccessKeySecret: "secret"},
|
||||
options: AccessOptions{Purpose: PurposeDisplay},
|
||||
wantMode: DeliveryOrigin, wantReason: "cdn_auth_unconfigured",
|
||||
},
|
||||
@@ -98,6 +100,12 @@ func TestResolveAccessPolicyMatrix(t *testing.T) {
|
||||
if access.Delivery != tt.wantMode || access.FallbackReason != tt.wantReason {
|
||||
t.Fatalf("ResolveAccess() = %#v, want mode=%q reason=%q", access, tt.wantMode, tt.wantReason)
|
||||
}
|
||||
if tt.wantMode == DeliveryOrigin {
|
||||
parsed, err := url.Parse(access.URL)
|
||||
if err != nil || parsed.Host != "private-bucket.s3.amazonaws.com" || parsed.Query().Get("Signature") == "" {
|
||||
t.Fatalf("invalid signed origin URL: %q (%v)", access.URL, err)
|
||||
}
|
||||
}
|
||||
if tt.wantURL != "" && access.URL != tt.wantURL {
|
||||
t.Fatalf("ResolveAccess().URL = %q, want %q", access.URL, tt.wantURL)
|
||||
}
|
||||
|
||||
69
backend/internal/outbound/outboundtest/resolver.go
Normal file
69
backend/internal/outbound/outboundtest/resolver.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Package outboundtest provides socket-free DNS fixtures for outbound policy tests.
|
||||
package outboundtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
// PublicDNS maps only the supplied hostnames to a public A record. Other names
|
||||
// return NXDOMAIN; AAAA queries return no records. No network socket is opened.
|
||||
// It changes process-global state: callers and their parents must not use
|
||||
// t.Parallel. Cleanup restores the previous resolver (including nested fixtures).
|
||||
func PublicDNS(t *testing.T, hosts ...string) {
|
||||
t.Helper()
|
||||
DNS(t, [4]byte{1, 1, 1, 1}, hosts...)
|
||||
}
|
||||
|
||||
// DNS maps supplied names to a chosen IPv4 address without opening sockets.
|
||||
func DNS(t *testing.T, address [4]byte, hosts ...string) {
|
||||
t.Helper()
|
||||
allowed := make(map[string]bool, len(hosts))
|
||||
for _, host := range hosts {
|
||||
allowed[strings.ToLower(strings.TrimSuffix(host, "."))+"."] = true
|
||||
}
|
||||
previous := net.DefaultResolver
|
||||
net.DefaultResolver = &net.Resolver{PreferGo: true, Dial: func(ctx context.Context, network, resolverAddress string) (net.Conn, error) {
|
||||
client, server := net.Pipe()
|
||||
go func() {
|
||||
defer server.Close()
|
||||
for {
|
||||
var size [2]byte
|
||||
if _, err := io.ReadFull(server, size[:]); err != nil {
|
||||
return
|
||||
}
|
||||
wire := make([]byte, binary.BigEndian.Uint16(size[:]))
|
||||
if _, err := io.ReadFull(server, wire); err != nil {
|
||||
return
|
||||
}
|
||||
var query dnsmessage.Message
|
||||
if query.Unpack(wire) != nil || len(query.Questions) != 1 {
|
||||
return
|
||||
}
|
||||
q := query.Questions[0]
|
||||
response := dnsmessage.Message{Header: dnsmessage.Header{ID: query.ID, Response: true, Authoritative: true, RecursionDesired: query.RecursionDesired, RecursionAvailable: true}, Questions: query.Questions}
|
||||
if !allowed[strings.ToLower(q.Name.String())] {
|
||||
response.RCode = dnsmessage.RCodeNameError
|
||||
} else if q.Type == dnsmessage.TypeA && q.Class == dnsmessage.ClassINET {
|
||||
response.Answers = []dnsmessage.Resource{{Header: dnsmessage.ResourceHeader{Name: q.Name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 60}, Body: &dnsmessage.AResource{A: address}}}
|
||||
}
|
||||
answer, err := response.Pack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
binary.BigEndian.PutUint16(size[:], uint16(len(answer)))
|
||||
if _, err := server.Write(append(size[:], answer...)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return client, nil
|
||||
}}
|
||||
t.Cleanup(func() { net.DefaultResolver = previous })
|
||||
}
|
||||
27
backend/internal/outbound/outboundtest/resolver_test.go
Normal file
27
backend/internal/outbound/outboundtest/resolver_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package outboundtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPublicDNS(t *testing.T) {
|
||||
previous := net.DefaultResolver
|
||||
t.Run("fixture", func(t *testing.T) {
|
||||
PublicDNS(t, "media.example.com")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", "media.example.com")
|
||||
if err != nil || len(ips) != 1 || !ips[0].Equal(net.IPv4(1, 1, 1, 1)) {
|
||||
t.Fatalf("A/AAAA lookup = %v, %v", ips, err)
|
||||
}
|
||||
if _, err := net.DefaultResolver.LookupIP(ctx, "ip", "unregistered.example.com"); err == nil {
|
||||
t.Fatal("unknown host must fail closed")
|
||||
}
|
||||
})
|
||||
if net.DefaultResolver != previous {
|
||||
t.Fatal("resolver was not restored")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user