Skip to content

Commit e11e029

Browse files
committed
add client tests to woo.sh repo
1 parent d6982cb commit e11e029

File tree

6 files changed

+2254
-0
lines changed

6 files changed

+2254
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ __pycache__/
55
venv*/
66
.cache/
77
generated-example-server/
8+
node_modules/

graphql-server/tests/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
To run the client tests, run:
2+
```bash
3+
$ npm install
4+
$ node client-tests.js
5+
```
6+
7+
The test framework will execute a subset of all possible operations on the server, using randomly generated data.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/**
2+
* This class is meant as basic functionality/integration test to find obvious errors in the generated
3+
* resolvers, schema, or backend code. Basically, it's originally meant as something of a functionality + sanity check.
4+
*
5+
* What it does:
6+
* For each GraphQL type, a random object input object is created. Nested input objects are created to a configurable
7+
* depth. After level 3, only required fields are included in the generated inputs. For every created object, the
8+
* object is retrieved with all possible subfields down to a configurable level based on its ID. Then the all fields
9+
* are updated in the same way as an object is created. Finally, for all types, a list objects is retrieved with all
10+
* possible subfields down to a configurable level. A simple equality filter is added for the ID of the type.
11+
*
12+
*
13+
* What id does NOT do:
14+
* - Does not verify that the returned object matches the expected result. (TODO)
15+
* - Executes no queries over annotated edges (TODO)
16+
* - Executes no mutations to annotate edges (TODO).
17+
* - ... a lot of other things probably.
18+
*/
19+
20+
const { InMemoryCache } = require('apollo-cache-inmemory')
21+
const { ApolloClient } = require('apollo-client');
22+
const { HttpLink } = require('apollo-link-http');
23+
const gql = require('graphql-tag');
24+
const { introspectSchema } = require('graphql-tools');
25+
const fetch = require('node-fetch');
26+
const { isObjectType } = require('graphql');
27+
const tools = require('./generation-tools.js');
28+
29+
/**
30+
* The transaction tests requires the database to be empty prior to testing.
31+
*/
32+
async function transactionTest() {
33+
// connect
34+
let uri = 'http://localhost:4000';
35+
let {client, schema} = await connect(uri);
36+
37+
// attempt to create objects but trigger an exception
38+
39+
// verify that not
40+
}
41+
42+
async function run() {
43+
// connect client to server
44+
let uri = 'http://localhost:4000';
45+
let {client, schema} = await connect(uri);
46+
47+
// iterate type schema
48+
for(let i in schema._typeMap){
49+
let t = schema._typeMap[i];
50+
if(!isObjectType(t) || i == 'Query' || i == 'Mutation' || i.startsWith('_') || i.includes('EdgeFrom')) continue;
51+
52+
// mutations to create
53+
let inputToCreate = tools.makeInputToCreate(t, schema, 6, true);
54+
let createArg = tools.jsonToGraphQL(inputToCreate);
55+
let create = `
56+
mutation {
57+
create${t.name}(data:${createArg}) {
58+
id
59+
}
60+
}
61+
`;
62+
console.log(`Mutations:\tcreate${t.name}`);
63+
const m1 = await client.mutate({ mutation: gql`${create}` });
64+
if(m1.errors){
65+
console.error(m1.errors);
66+
}
67+
68+
// query to get by ID
69+
let subfields = tools.getSubFields(t, 3);
70+
let id = m1.data[`create${t.name}`].id;
71+
let n = `${t.name[0].toLowerCase() + t.name.substr(1)}`;
72+
let get = `
73+
query {
74+
${n}(id: "${id}")
75+
${subfields}
76+
}
77+
`;
78+
console.log(`Query:\t\t${t.name[0].toLowerCase() + t.name.substr(1)} (ID=${id})`);
79+
const q1 = await client.query({ query: gql`${get}` });
80+
if(q1.errors){
81+
console.error(q1.errors);
82+
}
83+
84+
// update fields, create a new object and write over the values
85+
let inputToUpdate = tools.makeInputToCreate(t, schema, 7, true);
86+
let updateArg = tools.jsonToGraphQL(inputToUpdate);
87+
let mutation2 = `
88+
mutation {
89+
update${t.name}(id: "${id}", data:${updateArg}) {
90+
id
91+
}
92+
}
93+
`;
94+
console.log(`Mutation:\tupdate${t.name} (id=${id})`);
95+
const m2 = await client.mutate({mutation: gql`${mutation2}`});
96+
if (m2.errors) {
97+
console.error(m2.errors);
98+
}
99+
100+
// query list of type
101+
subfields = tools.getSubFields(t, 3);
102+
let getList = `
103+
query {
104+
listOf${t.name}s(first: 7, after: "", filter: { id: { _neq: "" } }) {
105+
isEndOfWholeList
106+
totalCount
107+
content
108+
${subfields}
109+
}
110+
}
111+
`;
112+
const q2 = await client.query({ query: gql`${getList}` });
113+
let totalCount = q2.data[`listOf${t.name}s`].totalCount;
114+
console.log(`Query:\t\tlistOf${t.name}s ${totalCount}`);
115+
if(q2.errors){
116+
console.error(q2.errors);
117+
}
118+
}
119+
}
120+
121+
async function connect(uri){
122+
const httpLink = new HttpLink({ uri: uri, fetch });
123+
const client = new ApolloClient({ link: httpLink, cache: new InMemoryCache() });
124+
const schema = await introspectSchema(httpLink); // parse remote schema
125+
return { client: client, schema: schema };
126+
}
127+
128+
run().then(() => {
129+
console.log("Client tests passed.");
130+
let exitAfterClientTests = process.env.EXIT_AFTER_CLIENT_TESTS === 'true';
131+
if(exitAfterClientTests) process.exit(0);
132+
}).catch(reason => {
133+
let exitAfterClientTests = process.env.EXIT_AFTER_CLIENT_TESTS === 'true';
134+
// Not the nicest way to exit, but it works for testing.
135+
console.error(reason);
136+
console.error("Client tests did NOT pass.");
137+
if(exitAfterClientTests) process.exit(1);
138+
});
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
const client = require('apollo-server-testing');
2+
3+
const graphql = require('graphql');
4+
const faker = require('faker');
5+
const gql = require('graphql-tag');
6+
7+
module.exports = {
8+
makeInputToCreate: (type, schema, limit, includeOptional=true) => makeInputToCreate(type, schema, limit, includeOptional),
9+
jsonToGraphQL: (ob) => jsonToGraphQL(ob),
10+
inputObjectToObjectType: (ob) => inputObjectToObject(ob),
11+
getSubFields: (ob, limit) => getSubFields(ob, 0, limit)
12+
}
13+
14+
let seed = 100;
15+
faker.seed(seed);
16+
17+
let scalarF = {
18+
'String' : faker.lorem.words,
19+
'Float' : Math.random,
20+
'Int' : faker.random.number,
21+
'Boolean' : faker.random.boolean,
22+
'Date' : faker.date.past
23+
};
24+
25+
/**
26+
* Generate a random example input to create a certain type down to some arbitrary depth. After three levels, only
27+
* required fields will be included!
28+
*
29+
* @param type
30+
* @param parents
31+
*/
32+
function generateInput(type, depth=0, limit=3, include_optional){
33+
let ob = {};
34+
for(let field_name in type._fields){
35+
// skip reverse edges
36+
if(field_name.startsWith('_')){
37+
continue;
38+
}
39+
40+
// generate value
41+
let value = null;
42+
let field = type._fields[field_name];
43+
let field_type = field.type;
44+
45+
// skip optional
46+
if(!include_optional || depth >= limit || depth >= 3){
47+
if(graphql.isNullableType(field_type)){
48+
continue;
49+
}
50+
}
51+
52+
let named_type = graphql.getNamedType(field.type);
53+
if(graphql.isEnumType(named_type)){
54+
value = '__ENUM__' + faker.random.arrayElement(named_type.getValues()).name;
55+
} else if(graphql.isScalarType(named_type)){
56+
if(scalarF[named_type.name]){
57+
value = scalarF[named_type.name]();
58+
} else {
59+
value = scalarF['String']();
60+
}
61+
} else {
62+
// check for loop
63+
if(depth >= limit){
64+
value = {'connect' : `Dummy/${faker.random.number()}` }
65+
} else {
66+
let keys = [];
67+
for(let k in named_type._fields){
68+
if(k == 'connect'){
69+
continue; // don't use connect
70+
}
71+
keys.push(k); // add a create or createX field
72+
}
73+
// pick the create field or one of the createX fields
74+
let key = faker.random.arrayElement(keys);
75+
let t = named_type._fields[key].type;
76+
77+
value = {}
78+
value[key] = generateInput(t, depth + 1, limit, include_optional);
79+
}
80+
}
81+
ob[field_name] = graphql.isListType(field_type) ? [value] : value;
82+
83+
}
84+
return ob;
85+
}
86+
87+
function makeInputToCreate(type, schema, limit, include_optional){
88+
let arg_type = schema._typeMap[`_InputToCreate${type.name}`]
89+
return generateInput(arg_type, 0, limit, include_optional);
90+
}
91+
92+
/**
93+
* Returns a GraphQL formatted string, where any quoted props and enums are unquoted.
94+
* @param ob
95+
* @returns {Promise<any> | void | string}
96+
*/
97+
function jsonToGraphQL(ob){
98+
let string = JSON.stringify(ob, null, 2);
99+
string = string.replace(/\"([^(\")"]+)\":/g,"$1:");
100+
string = string.replace(/\"__ENUM__([^(\")"]+)\"/g,"$1");
101+
return string
102+
}
103+
104+
/**
105+
* Substitute and flatten any nested connect/create field in a JSON object to form the corresponding type
106+
* representation.
107+
* @param ob
108+
* @returns
109+
*/
110+
function inputObjectToObject(ob){
111+
ob = copy(ob);
112+
let k = Object.keys(ob)[0];
113+
if(k === undefined){
114+
return ob;
115+
}
116+
117+
if(k == 'connect'){
118+
return { id: ob['connect'] };
119+
} else if(k.startsWith('create')){
120+
return inputObjectToObject(ob[k]);
121+
} else {
122+
for(let i in ob){
123+
if(Array.isArray(ob[i])){
124+
let a = [];
125+
for(let j in ob[i]){
126+
a.push(inputObjectToObject(ob[i][j]));
127+
}
128+
ob[i] = a;
129+
} else if(typeof ob[i] === "object"){
130+
ob[i] = inputObjectToObject(ob[i]);
131+
} else {
132+
continue;
133+
}
134+
}
135+
}
136+
137+
return ob;
138+
}
139+
140+
141+
/**
142+
* Returns a GraphQL string representing the set of subfields present in GraphQL object.
143+
* @param ob
144+
*/
145+
function getSubFields(type, depth=0, limit=3){
146+
let subfields = ' { ';
147+
for(let f in type.getFields()){
148+
let t = graphql.getNamedType(type.getFields()[f].type);
149+
150+
if((graphql.isObjectType(t) || graphql.isInterfaceType(t)) && depth >= limit){
151+
continue;
152+
}
153+
154+
subfields += ` ${f} `;
155+
if(graphql.isObjectType(t) || graphql.isInterfaceType(t)){
156+
subfields += getSubFields(t, depth + 1, limit);
157+
}
158+
}
159+
subfields += ' } ';
160+
return subfields;
161+
}
162+
163+
/**
164+
* Copy JSON type using stringify and parse.
165+
* @param x
166+
* @returns {any}
167+
*/
168+
function copy(x) {
169+
return JSON.parse(JSON.stringify(x));
170+
}

0 commit comments

Comments
 (0)