Skip to content

Commit 8e14df8

Browse files
Add federation integration test
1 parent 79dd2f3 commit 8e14df8

4 files changed

Lines changed: 254 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Federation test setup with 2 alpha instances
2+
# alpha1: user-service subgraph
3+
# alpha2: reviews-service subgraph
4+
services:
5+
alpha1:
6+
image: dgraph/dgraph:local
7+
working_dir: /data/alpha1
8+
labels:
9+
cluster: test
10+
service: alpha1
11+
ports:
12+
- 8080
13+
- 9080
14+
volumes:
15+
- type: bind
16+
source: ${LINUX_GOBIN:-$GOPATH/bin}
17+
target: /gobin
18+
read_only: true
19+
command:
20+
/gobin/dgraph ${COVERAGE_OUTPUT} alpha --my=alpha1:7080 --zero=zero1:5080 --logtostderr -v=2
21+
--raft="idx=1;" --security "whitelist=0.0.0.0/0;"
22+
alpha2:
23+
image: dgraph/dgraph:local
24+
working_dir: /data/alpha2
25+
depends_on:
26+
- alpha1
27+
labels:
28+
cluster: test
29+
service: alpha2
30+
ports:
31+
- 8080
32+
- 9080
33+
volumes:
34+
- type: bind
35+
source: ${LINUX_GOBIN:-$GOPATH/bin}
36+
target: /gobin
37+
read_only: true
38+
command:
39+
/gobin/dgraph ${COVERAGE_OUTPUT} alpha --my=alpha2:7080 --zero=zero1:5080 --logtostderr -v=2
40+
--raft="idx=2;" --security "whitelist=0.0.0.0/0;"
41+
zero1:
42+
image: dgraph/dgraph:local
43+
working_dir: /data/zero1
44+
labels:
45+
cluster: test
46+
ports:
47+
- 5080
48+
- 6080
49+
volumes:
50+
- type: bind
51+
source: ${LINUX_GOBIN:-$GOPATH/bin}
52+
target: /gobin
53+
read_only: true
54+
command:
55+
/gobin/dgraph ${COVERAGE_OUTPUT} zero --telemetry "reports=false;" --raft="idx=1;"
56+
--my=zero1:5080 --replicas=1 --logtostderr -v=2 --bindall
57+
volumes: {}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//go:build integration
2+
3+
/*
4+
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
5+
* SPDX-License-Identifier: Apache-2.0
6+
*/
7+
8+
package federation
9+
10+
import (
11+
"context"
12+
"encoding/json"
13+
"fmt"
14+
"net/http"
15+
"os"
16+
"os/exec"
17+
"path/filepath"
18+
"strings"
19+
"testing"
20+
"time"
21+
22+
"github.com/stretchr/testify/require"
23+
24+
"github.com/dgraph-io/dgraph/v25/graphql/e2e/common"
25+
"github.com/dgraph-io/dgraph/v25/testutil"
26+
"github.com/dgraph-io/dgraph/v25/x"
27+
)
28+
29+
var (
30+
// Alpha1 serves the user-service subgraph
31+
Alpha1HTTP = testutil.ContainerAddr("alpha1", 8080)
32+
Alpha1GraphqlURL = "http://" + Alpha1HTTP + "/graphql"
33+
Alpha1GraphqlAdminURL = "http://" + Alpha1HTTP + "/admin"
34+
35+
// Alpha2 serves the reviews-service subgraph
36+
Alpha2HTTP = testutil.ContainerAddr("alpha2", 8080)
37+
Alpha2GraphqlURL = "http://" + Alpha2HTTP + "/graphql"
38+
Alpha2GraphqlAdminURL = "http://" + Alpha2HTTP + "/admin"
39+
)
40+
41+
// TestMain sets up the test environment with two Dgraph alpha instances
42+
func TestMain(m *testing.M) {
43+
userSchema, err := os.ReadFile("testdata/user-service.graphql")
44+
x.Panic(err)
45+
46+
reviewsSchema, err := os.ReadFile("testdata/reviews-service.graphql")
47+
x.Panic(err)
48+
49+
// Wait for both alphas to be ready
50+
err = common.CheckGraphQLStarted(Alpha1GraphqlAdminURL)
51+
x.Panic(err)
52+
err = common.CheckGraphQLStarted(Alpha2GraphqlAdminURL)
53+
x.Panic(err)
54+
55+
// Bootstrap alpha1 with user-service schema
56+
common.BootstrapServer(userSchema, nil)
57+
58+
// Deploy reviews-service schema to alpha2
59+
// Retry until schema is successfully deployed
60+
maxRetries := 30
61+
for i := 0; i < maxRetries; i++ {
62+
resp, updateErr := http.Post("http://"+Alpha2HTTP+"/admin/schema",
63+
"application/graphql", strings.NewReader(string(reviewsSchema)))
64+
if updateErr == nil {
65+
resp.Body.Close()
66+
if resp.StatusCode == 200 {
67+
// Wait a bit for schema to be loaded
68+
time.Sleep(500 * time.Millisecond)
69+
break
70+
}
71+
}
72+
if i == maxRetries-1 {
73+
x.Panic(fmt.Errorf("failed to deploy schema to alpha2 after %d retries", maxRetries))
74+
}
75+
time.Sleep(200 * time.Millisecond)
76+
}
77+
78+
os.Exit(m.Run())
79+
}
80+
81+
// TestRoverFederationComposition tests Apollo Federation supergraph composition
82+
// with two Dgraph instances serving different federated subgraphs.
83+
//
84+
// This test:
85+
// 1. Fetches SDL via _service query from alpha1 (user-service) and alpha2 (reviews-service)
86+
// 2. Runs Apollo Rover CLI to compose a supergraph
87+
// 3. Verifies composition succeeds (auxiliary types should be excluded for @extends types)
88+
func TestRoverFederationComposition(t *testing.T) {
89+
90+
// Test that we can run docker
91+
testCmd := exec.Command("docker", "version")
92+
if err := testCmd.Run(); err != nil {
93+
t.Skip("Skipping test: docker is not running or accessible")
94+
}
95+
96+
testdataDir := "./testdata"
97+
absTestdataDir, err := filepath.Abs(testdataDir)
98+
require.NoError(t, err)
99+
100+
// Fetch SDL from alpha1 (user-service)
101+
serviceQuery := &common.GraphQLParams{
102+
Query: `query { _service { sdl } }`,
103+
}
104+
105+
userServiceResp := serviceQuery.ExecuteAsPost(t, Alpha1GraphqlURL)
106+
common.RequireNoGQLErrors(t, userServiceResp)
107+
108+
var userServiceResult struct {
109+
Service struct {
110+
SDL string `json:"sdl"`
111+
} `json:"_service"`
112+
}
113+
require.NoError(t, json.Unmarshal(userServiceResp.Data, &userServiceResult))
114+
115+
// Fetch SDL from alpha2 (reviews-service)
116+
reviewsServiceResp := serviceQuery.ExecuteAsPost(t, Alpha2GraphqlURL)
117+
common.RequireNoGQLErrors(t, reviewsServiceResp)
118+
119+
var reviewsServiceResult struct {
120+
Service struct {
121+
SDL string `json:"sdl"`
122+
} `json:"_service"`
123+
}
124+
require.NoError(t, json.Unmarshal(reviewsServiceResp.Data, &reviewsServiceResult))
125+
126+
userSDL := userServiceResult.Service.SDL
127+
reviewsSDL := reviewsServiceResult.Service.SDL
128+
129+
// Write the SDL files that Rover will use
130+
userSDLFile := filepath.Join(testdataDir, "user-service-generated.graphql")
131+
require.NoError(t, os.WriteFile(userSDLFile, []byte(userSDL), 0644))
132+
defer os.Remove(userSDLFile)
133+
134+
reviewsSDLFile := filepath.Join(testdataDir, "reviews-service-generated.graphql")
135+
require.NoError(t, os.WriteFile(reviewsSDLFile, []byte(reviewsSDL), 0644))
136+
defer os.Remove(reviewsSDLFile)
137+
138+
// Create supergraph config that points to the generated SDL files
139+
supergraphConfig := fmt.Sprintf(`federation_version: =2.7.1
140+
subgraphs:
141+
users:
142+
routing_url: %s
143+
schema:
144+
file: ./user-service-generated.graphql
145+
reviews:
146+
routing_url: %s
147+
schema:
148+
file: ./reviews-service-generated.graphql
149+
`, Alpha1GraphqlURL, Alpha2GraphqlURL)
150+
supergraphFile := filepath.Join(testdataDir, "supergraph-generated.yml")
151+
require.NoError(t, os.WriteFile(supergraphFile, []byte(supergraphConfig), 0644))
152+
defer os.Remove(supergraphFile)
153+
154+
// Run rover supergraph compose using Docker
155+
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
156+
defer cancel()
157+
158+
cmd := exec.CommandContext(ctx, "docker", "run", "--rm",
159+
"-e", "APOLLO_ELV2_LICENSE=accept",
160+
"-v", absTestdataDir+":/workspace",
161+
"-w", "/workspace",
162+
"node:18-slim",
163+
"sh", "-c",
164+
`apt-get update -qq && apt-get install -y -qq curl > /dev/null 2>&1 && \
165+
curl -sSL https://rover.apollo.dev/nix/latest | sh > /dev/null 2>&1 && \
166+
$HOME/.rover/bin/rover supergraph compose --config supergraph-generated.yml`)
167+
168+
output, err := cmd.CombinedOutput()
169+
outputStr := string(output)
170+
171+
// The auxiliary types for extended User should NOT be generated from alpha2 (reviews-service)
172+
if err != nil {
173+
if strings.Contains(outputStr, "UserPatch") ||
174+
strings.Contains(outputStr, "UserOrderable") ||
175+
strings.Contains(outputStr, "UserHasFilter") {
176+
t.Errorf("Rover composition failed with auxiliary type conflicts. "+
177+
"The fix should exclude these types for @extends. Error: %v", err)
178+
} else {
179+
t.Errorf("Rover composition failed with unexpected error: %v", err)
180+
}
181+
}
182+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
type Review {
2+
id: ID!
3+
rating: Int! @search
4+
comment: String @search(by: [fulltext])
5+
}
6+
7+
extend type User @key(fields: "userId") {
8+
userId: ID! @external
9+
reviews: [Review]
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
type User @key(fields: "userId") {
2+
userId: ID!
3+
username: String! @search(by: [hash])
4+
email: String
5+
}

0 commit comments

Comments
 (0)