Browse Source

More tests; Bugfixes;

bjorn 9 năm trước cách đây
mục cha
commit
7b81a16924
5 tập tin đã thay đổi với 234 bổ sung42 xóa
  1. 2 2
      parse.lua
  2. 11 10
      rules.lua
  3. 3 1
      tests/data/schema.lua
  4. 213 28
      tests/rules.lua
  5. 5 1
      validate.lua

+ 2 - 2
parse.lua

@@ -239,7 +239,7 @@ end
 -- Simple types
 local rawName = R('az', 'AZ') * (P'_' + R'09' + R('az', 'AZ')) ^ 0
 local name = rawName / cName
-local fragmentName = (rawName - 'on') / cName
+local fragmentName = (rawName - ('on' * -rawName)) / cName
 local alias = ws * name * P':' * ws / cAlias
 
 local integerPart = P'-' ^ -1 * ('0' + R'19' * R'09' ^ 0)
@@ -307,7 +307,7 @@ local function stripComments(str)
     end
 
     return line
-  end)
+  end):sub(1, -2)
 end
 
 return function(str)

+ 11 - 10
rules.lua

@@ -139,21 +139,18 @@ function rules.unambiguousSelections(node, context)
           definition = definition
         }
 
-        if seen[definition] then
-          return
-        end
-
-        seen[definition] = true
-
         validateField(key, fieldEntry)
       elseif selection.kind == 'inlineFragment' then
         local parentType = selection.typeCondition and context.schema:getType(selection.typeCondition.name.value) or parentType
         validateSelectionSet(selection.selectionSet, parentType)
       elseif selection.kind == 'fragmentSpread' then
         local fragmentDefinition = context.fragmentMap[selection.name.value]
-        if fragmentDefinition and fragmentDefinition.typeCondition then
-          local parentType = context.schema:getType(fragmentDefinition.typeCondition.name.value)
-          validateSelectionSet(fragmentDefinition.selectionSet, parentType)
+        if not seen[fragmentDefinition] then
+          seen[fragmentDefinition] = true
+          if fragmentDefinition and fragmentDefinition.typeCondition then
+            local parentType = context.schema:getType(fragmentDefinition.typeCondition.name.value)
+            validateSelectionSet(fragmentDefinition.selectionSet, parentType)
+          end
         end
       end
     end
@@ -414,10 +411,14 @@ function rules.variableUsageAllowed(node, context)
     if node.kind == 'field' then
       arguments = { [node.name.value] = node.arguments }
     elseif node.kind == 'fragmentSpread' then
+      local seen = {}
       local function collectArguments(referencedNode)
         if referencedNode.kind == 'selectionSet' then
           for _, selection in ipairs(referencedNode.selections) do
-            collectArguments(selection)
+            if not seen[selection] then
+              seen[selection] = true
+              collectArguments(selection)
+            end
           end
         elseif referencedNode.kind == 'field' and referencedNode.arguments then
           local fieldName = referencedNode.name.value

+ 3 - 1
tests/data/schema.lua

@@ -97,7 +97,9 @@ local query = types.object({
           kind = types.string
         }
       }
-    }
+    },
+    pet = pet,
+    catOrDog = catOrDog
   }
 })
 

+ 213 - 28
tests/rules.lua

@@ -2,70 +2,255 @@ local parse = require 'parse'
 local validate = require 'validate'
 local schema = require 'tests/data/schema'
 
-local function run(query)
-  validate(schema, parse(query))
+local function expectError(message, document)
+  if not message then
+    expect(function() validate(schema, parse(document)) end).to_not.fail()
+  else
+    expect(function() validate(schema, parse(document)) end).to.fail.with(message)
+  end
 end
 
 describe('rules', function()
+  local document
+
   describe('uniqueOperationNames', function()
+    local message = 'Multiple operations exist named'
+
     it('errors if two operations have the same name', function()
-      local query = [[
+      expectError(message, [[
         query foo { }
         query foo { }
-      ]]
-
-      expect(function() run(query) end).to.fail.with('Multiple operations exist named')
+      ]])
     end)
 
-    it('passes if operations have different names', function()
-      local query = [[
+    it('passes if all operations have different names', function()
+      expectError(nil, [[
         query foo { }
         query bar { }
-      ]]
+      ]])
+    end)
+  end)
+
+  describe('loneAnonymousOperation', function()
+    local message = 'Cannot have more than one operation when'
+
+    it('fails if there is more than one operation and one of them is anonymous', function()
+      expectError(message, [[
+        query { }
+        query named { }
+      ]])
+
+      expectError(message, [[
+        query named { }
+        query { }
+      ]])
+
+      expectError(message, [[
+        query { }
+        query { }
+      ]])
+    end)
+
+    it('passes if there is one anonymous operation', function()
+      expectError(nil, '{}')
+    end)
 
-      expect(function() run(query) end).to_not.fail()
+    it('passes if there are two named operations', function()
+      expectError(nil, [[
+        query one {}
+        query two {}
+      ]])
+    end)
+  end)
+
+  describe('fieldsDefinedOnType', function()
+    local message = 'is not defined on type'
+
+    it('fails if a field does not exist on an object type', function()
+      expectError(message, '{ doggy { name } }')
+      expectError(message, '{ dog { age } }')
+    end)
+
+    it('passes if all fields exist on object types', function()
+      expectError(nil, '{ dog { name } }')
+    end)
+
+    it('understands aliases', function()
+      expectError(nil, '{ doggy: dog { name } }')
+      expectError(message, '{ dog: doggy { name } }')
     end)
   end)
 
   describe('argumentsDefinedOnType', function()
-    it('passes if no arguments are supplied', function()
-      local query = [[{
-        dog {
-          isHouseTrained
-        }
-      }]]
+    local message = 'Non%-existent argument'
 
-      expect(function() run(query) end).to_not.fail()
+    it('passes if no arguments are supplied', function()
+      expectError(nil, '{ dog { isHouseTrained } }')
     end)
 
     it('errors if an argument name does not match the schema', function()
-      local query = [[{
+      expectError(message, [[{
         dog {
           doesKnowCommand(doggyCommand: SIT)
         }
-      }]]
-
-      expect(function() run(query) end).to.fail.with('Non%-existent argument')
+      }]])
     end)
 
     it('errors if an argument is supplied to a field that takes none', function()
-      local query = [[{
+      expectError(message, [[{
         dog {
           name(truncateToLength: 32)
         }
-      }]]
-
-      expect(function() run(query) end).to.fail.with('Non%-existent argument')
+      }]])
     end)
 
     it('passes if all argument names match the schema', function()
-      local query = [[{
+      expectError(nil, [[{
         dog {
           doesKnowCommand(dogCommand: SIT)
         }
-      }]]
+      }]])
+    end)
+  end)
+
+  describe('scalarFieldsAreLeaves', function()
+    local message = 'Scalar values cannot have subselections'
+
+    it('fails if a scalar field has a subselection', function()
+      expectError(message, '{ dog { name { firstLetter } } }')
+    end)
+
+    it('passes if all scalar fields are leaves', function()
+      expectError(nil, '{ dog { name nickname } }')
+    end)
+  end)
+
+  describe('compositeFieldsAreNotLeaves', function()
+    local message = 'Composite types must have subselections'
+    
+    it('fails if an object is a leaf', function()
+      expectError(message, '{ dog }')
+    end)
+
+    it('fails if an interface is a leaf', function()
+      expectError(message, '{ pet }')
+    end)
+
+    it('fails if a union is a leaf', function()
+      expectError(message, '{ catOrDog }')
+    end)
+
+    it('passes if all composite types have subselections', function()
+      expectError(nil, '{ dog { name } pet { } }')
+    end)
+  end)
+
+  describe('unambiguousSelections', function()
+    it('fails if two fields with identical response keys have different types', function()
+      expectError('Type name mismatch', [[{
+        dog {
+          barkVolume
+          barkVolume: name
+        }
+      }]])
+    end)
+
+    it('fails if two fields have different argument sets', function()
+      expectError('Argument mismatch', [[{
+        dog {
+          doesKnowCommand(dogCommand: SIT)
+          doesKnowCommand(dogCommand: DOWN)
+        }
+      }]])
+    end)
+
+    it('passes if fields are identical', function()
+      expectError(nil, [[{
+        dog {
+          doesKnowCommand(dogCommand: SIT)
+          doesKnowCommand: doesKnowCommand(dogCommand: SIT)
+        }
+      }]])
+    end)
+  end)
+
+  describe('uniqueArgumentNames', function()
+    local message = 'Encountered multiple arguments named'
+
+    it('fails if a field has two arguments with the same name', function()
+      expectError(message, [[{
+        dog {
+          doesKnowCommand(dogCommand: SIT, dogCommand: DOWN)
+        }
+      }]])
+    end)
+  end)
+
+  describe('argumentsOfCorrectType', function()
+    it('fails if an argument has an incorrect type', function()
+      expectError('Expected enum value', [[{
+        dog {
+          doesKnowCommand(dogCommand: 4)
+        }
+      }]])
+    end)
+  end)
+
+  describe('requiredArgumentsPresent', function()
+    local message = 'was not supplied'
+
+    it('fails if a non-null argument is not present', function()
+      expectError(message, [[{
+        dog {
+          doesKnowCommand
+        }
+      }]])
+    end)
+  end)
+
+  describe('uniqueFragmentNames', function()
+    local message = 'Encountered multiple fragments named'
+
+    it('fails if there are two fragment definitions with the same name', function()
+      expectError(message, [[
+        query { dog { ...nameFragment } }
+        fragment nameFragment on Dog { name }
+        fragment nameFragment on Dog { name }
+      ]])
+    end)
+
+    it('passes if all fragment definitions have different names', function()
+      expectError(nil, [[
+        query { dog { ...one ...two } }
+        fragment one on Dog { name }
+        fragment two on Dog { name }
+      ]])
+    end)
+  end)
+
+  describe('fragmentHasValidType', function()
+    it('fails if a framgent refers to a non-composite type', function()
+      expectError('Fragment type must be an Object, Interface, or Union', 'fragment f on DogCommand {}')
+    end)
+
+    it('fails if a fragment refers to a non-existent type', function()
+      expectError('Fragment refers to non%-existent type', 'fragment f on Hyena {}')
+    end)
+
+    it('passes if a fragment refers to a composite type', function()
+      expectError(nil, '{ dog { ...f } } fragment f on Dog {}')
+    end)
+  end)
+
+  describe('noUnusedFragments', function()
+    local message = 'was not used'
+
+    it('fails if a fragment is not used', function()
+      expectError(message, 'fragment f on Dog {}')
+    end)
 
-      expect(function() run(query) end).to_not.fail()
+    it('passes if every fragment is used', function()
+      expectError(nil, '{ dog { ...f } } fragment f on Dog {}')
     end)
   end)
 end)

+ 5 - 1
validate.lua

@@ -142,12 +142,16 @@ local visitors = {
       table.insert(context.objects, fragmentType)
 
       if context.currentOperation then
+        local seen = {}
         local function collectTransitiveVariables(referencedNode)
           if not referencedNode then return end
 
           if referencedNode.kind == 'selectionSet' then
             for _, selection in ipairs(referencedNode.selections) do
-              collectTransitiveVariables(selection)
+              if not seen[selection] then
+                seen[selection] = true
+                collectTransitiveVariables(selection)
+              end
             end
           elseif referencedNode.kind == 'field' and referencedNode.arguments then
             for _, argument in ipairs(referencedNode.arguments) do