github.com/CiscoM31/godata@v1.0.10/url_parser_test.go (about)

     1  package godata
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/url"
     7  	"regexp"
     8  	"testing"
     9  )
    10  
    11  func TestUrlParser(t *testing.T) {
    12  	testUrl := "Employees(1)/Sales.Manager?$expand=DirectReports%28$select%3DFirstName%2CLastName%3B$levels%3D4%29"
    13  	parsedUrl, err := url.Parse(testUrl)
    14  
    15  	if err != nil {
    16  		t.Error(err)
    17  		return
    18  	}
    19  	ctx := context.Background()
    20  	request, err := ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
    21  
    22  	if err != nil {
    23  		t.Error(err)
    24  		return
    25  	}
    26  
    27  	if request.FirstSegment.Name != "Employees" {
    28  		t.Error("First segment is '" + request.FirstSegment.Name + "' not Employees")
    29  		return
    30  	}
    31  	if request.FirstSegment.Identifier.Get() != "1" {
    32  		t.Error("Employee identifier not found")
    33  		return
    34  	}
    35  	if request.FirstSegment.Next.Name != "Sales.Manager" {
    36  		t.Error("Second segment is not Sales.Manager")
    37  		return
    38  	}
    39  }
    40  
    41  func TestUrlParserStrictValidation(t *testing.T) {
    42  	testUrl := "Employees(1)/Sales.Manager?$expand=DirectReports%28$select%3DFirstName%2CLastName%3B$levels%3D4%29"
    43  	parsedUrl, err := url.Parse(testUrl)
    44  	if err != nil {
    45  		t.Error(err)
    46  		return
    47  	}
    48  	ctx := context.Background()
    49  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
    50  	if err != nil {
    51  		t.Error(err)
    52  		return
    53  	}
    54  
    55  	testUrl = "Employees(1)/Sales.Manager?$filter=FirstName eq 'Bob'"
    56  	parsedUrl, err = url.Parse(testUrl)
    57  	if err != nil {
    58  		t.Error(err)
    59  		return
    60  	}
    61  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
    62  	if err != nil {
    63  		t.Error(err)
    64  		return
    65  	}
    66  
    67  	// Wrong filter with an extraneous single quote
    68  	testUrl = "Employees(1)/Sales.Manager?$filter=FirstName eq' 'Bob'"
    69  	parsedUrl, err = url.Parse(testUrl)
    70  	if err != nil {
    71  		t.Error(err)
    72  		return
    73  	}
    74  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
    75  	if err == nil {
    76  		t.Errorf("Parser should have returned invalid filter error: %s", testUrl)
    77  		return
    78  	}
    79  
    80  	// Valid query with two parameters:
    81  	// $filter=FirstName eq 'Bob'
    82  	// at=Version eq '123'
    83  	testUrl = "Employees(1)/Sales.Manager?$filter=FirstName eq 'Bob'&at=Version eq '123'"
    84  	parsedUrl, err = url.Parse(testUrl)
    85  	if err != nil {
    86  		t.Error(err)
    87  		return
    88  	}
    89  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
    90  	if err != nil {
    91  		t.Error(err)
    92  		return
    93  	}
    94  
    95  	// Invalid query:
    96  	// $filter=FirstName eq' 'Bob' has extraneous single quote.
    97  	// at=Version eq '123'         is valid
    98  	testUrl = "Employees(1)/Sales.Manager?$filter=FirstName eq' 'Bob'&at=Version eq '123'"
    99  	parsedUrl, err = url.Parse(testUrl)
   100  	if err != nil {
   101  		t.Error(err)
   102  		return
   103  	}
   104  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
   105  	if err == nil {
   106  		t.Errorf("Parser should have returned invalid filter error: %s", testUrl)
   107  		return
   108  	}
   109  
   110  	testUrl = "Employees(1)/Sales.Manager?$select=3DFirstName"
   111  	parsedUrl, err = url.Parse(testUrl)
   112  	if err != nil {
   113  		t.Error(err)
   114  		return
   115  	}
   116  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
   117  	if err != nil {
   118  		t.Error(err)
   119  		return
   120  	}
   121  
   122  	testUrl = "Employees(1)/Sales.Manager?$filter=Name in ('Bob','Alice')&$select=Name,Address&$expand=Address($select=City)"
   123  	parsedUrl, err = url.Parse(testUrl)
   124  	if err != nil {
   125  		t.Error(err)
   126  		return
   127  	}
   128  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
   129  	if err != nil {
   130  		t.Errorf("Unexpected parsing error: %v", err)
   131  		return
   132  	}
   133  
   134  	// A $select option cannot be wrapped with parenthesis. This is not legal ODATA.
   135  
   136  	/*
   137  		 queryOptions = queryOption *( "&" queryOption )
   138  		 queryOption  = systemQueryOption
   139  				/ aliasAndValue
   140  				/ nameAndValue
   141  				/ customQueryOption
   142  		 systemQueryOption = compute
   143  				/ deltatoken
   144  				/ expand
   145  				/ filter
   146  				/ format
   147  				/ id
   148  				/ inlinecount
   149  				/ orderby
   150  				/ schemaversion
   151  				/ search
   152  				/ select
   153  				/ skip
   154  				/ skiptoken
   155  				/ top
   156  				/ index
   157  		  select = ( "$select" / "select" ) EQ selectItem *( COMMA selectItem )
   158  	*/
   159  	testUrl = "Employees(1)/Sales.Manager?$filter=Name in ('Bob','Alice')&($select=Name,Address%3B$expand=Address($select=City))"
   160  	parsedUrl, err = url.Parse(testUrl)
   161  	if err != nil {
   162  		t.Error(err)
   163  		return
   164  	}
   165  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
   166  	if err == nil {
   167  		t.Errorf("Parser should have raised error")
   168  		return
   169  	}
   170  
   171  	// Duplicate keyword: '$select' is present twice.
   172  	testUrl = "Employees(1)/Sales.Manager?$select=3DFirstName&$select=3DFirstName"
   173  	parsedUrl, err = url.Parse(testUrl)
   174  	if err != nil {
   175  		t.Error(err)
   176  		return
   177  	}
   178  	// In lenient mode, do not return an error when there is a duplicate keyword.
   179  	lenientContext := WithOdataComplianceConfig(ctx, ComplianceIgnoreAll)
   180  	_, err = ParseRequest(lenientContext, parsedUrl.Path, parsedUrl.Query())
   181  	if err != nil {
   182  		t.Error(err)
   183  		return
   184  	}
   185  	// In strict mode, return an error when there is a duplicate keyword.
   186  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
   187  	if err == nil {
   188  		t.Error("Parser should have returned duplicate keyword error")
   189  		return
   190  	}
   191  
   192  	// Unsupported keywords
   193  	testUrl = "Employees(1)/Sales.Manager?orderby=FirstName"
   194  	parsedUrl, err = url.Parse(testUrl)
   195  	if err != nil {
   196  		t.Error(err)
   197  		return
   198  	}
   199  	_, err = ParseRequest(lenientContext, parsedUrl.Path, parsedUrl.Query())
   200  	if err != nil {
   201  		t.Error(err)
   202  		return
   203  	}
   204  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
   205  	if err == nil {
   206  		t.Error("Parser should have returned unsupported keyword error")
   207  		return
   208  	}
   209  
   210  	testUrl = "Employees(1)/Sales.Manager?$select=LastName&$expand=Address"
   211  	parsedUrl, err = url.Parse(testUrl)
   212  	if err != nil {
   213  		t.Error(err)
   214  		return
   215  	}
   216  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
   217  	if err != nil {
   218  		t.Error(err)
   219  		return
   220  	}
   221  
   222  	testUrl = "Employees(1)/Sales.Manager?$select=FirstName,LastName&$expand=Address"
   223  	parsedUrl, err = url.Parse(testUrl)
   224  	if err != nil {
   225  		t.Error(err)
   226  		return
   227  	}
   228  	_, err = ParseRequest(ctx, parsedUrl.Path, parsedUrl.Query())
   229  	if err != nil {
   230  		t.Error(err)
   231  		return
   232  	}
   233  
   234  }
   235  
   236  // TestUnescapeStringTokens tests string encoding rules specified in the ODATA ABNF:
   237  // http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_URLSyntax
   238  func TestUnescapeStringTokens(t *testing.T) {
   239  
   240  	testCases := []struct {
   241  		url string // The test URL
   242  		// Set to nil if no error is expected.
   243  		// If error is expected, it is used to match err.Error()
   244  		errRegex *regexp.Regexp
   245  
   246  		expectedFilterTree []expectedParseNode
   247  		expectedOrderBy    []OrderByItem
   248  		expectedCompute    []ComputeItem
   249  	}{
   250  		{
   251  			// Unescaped single quotes.
   252  			// This is not a valid filter because:
   253  			// 1. there are two consecutive literal values, 'ab' and 'c,
   254  			// 2. 'c is not terminated with a quote.
   255  			url:      "/Books?$filter=Description eq 'ab'c'",
   256  			errRegex: regexp.MustCompile("Token ''' is invalid"),
   257  		},
   258  		{
   259  			// Simple string with special characters.
   260  			url:      "/Books?$filter=Description eq 'abc'",
   261  			errRegex: nil,
   262  			expectedFilterTree: []expectedParseNode{
   263  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   264  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   265  				{Value: "'abc'", Depth: 1, Type: ExpressionTokenString},
   266  			},
   267  		},
   268  		{
   269  			// Two consecutive single quotes.
   270  			// One of the URL syntax rules for ODATA is that single quotes within string
   271  			// literals are represented as two consecutive single quotes.
   272  			// This is done to make the input strings in the ABNF test cases more readable.
   273  			url:      "/Books?$filter=Description eq 'ab''c'",
   274  			errRegex: nil,
   275  			expectedFilterTree: []expectedParseNode{
   276  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   277  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   278  				// Note below two consecutive single-quotes are the encoding of one single quote,
   279  				// so after the tokenization it is unescaped to one single quote.
   280  				{Value: "'ab'c'", Depth: 1, Type: ExpressionTokenString},
   281  			},
   282  		},
   283  		{
   284  			// Test single quotes escaped as %27.
   285  			url:      "/Books?$filter=Description eq 'O%27%27Neil'",
   286  			errRegex: nil,
   287  			expectedFilterTree: []expectedParseNode{
   288  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   289  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   290  				// Percent-encoded character %27 must be decoded to single quote.
   291  				{Value: "'O'Neil'", Depth: 1, Type: ExpressionTokenString},
   292  			},
   293  		},
   294  		{
   295  			// Test single quotes escaped as %27.
   296  			// This time all single quotes are percent-encoded, including the outer single-quotes.
   297  			url:      "/Books?$filter=Description eq %27O%27%27Neil%27",
   298  			errRegex: nil,
   299  			expectedFilterTree: []expectedParseNode{
   300  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   301  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   302  				// Percent-encoded character %27 must be decoded to single quote.
   303  				{Value: "'O'Neil'", Depth: 1, Type: ExpressionTokenString},
   304  			},
   305  		},
   306  		{
   307  			// According to RFC 1738, URLs should not include UTF-8 characters,
   308  			// but the string tokens are parsed anyway.
   309  			url:      "/Books?$filter=Description eq '♺⛺⛵⚡'",
   310  			errRegex: nil,
   311  			expectedFilterTree: []expectedParseNode{
   312  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   313  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   314  				// Percent-encoded character %27 must be decoded to single quote.
   315  				{Value: "'♺⛺⛵⚡'", Depth: 1, Type: ExpressionTokenString},
   316  			},
   317  		},
   318  		{
   319  			// Strings with percent encoding
   320  			url:      "/Books?$filter=Description eq '%34%35%36'",
   321  			errRegex: nil,
   322  			expectedFilterTree: []expectedParseNode{
   323  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   324  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   325  				{Value: "'456'", Depth: 1, Type: ExpressionTokenString},
   326  			},
   327  		},
   328  		{
   329  			url:      "/Books?$filter=Description eq 'abc'&$orderby=Title",
   330  			errRegex: nil,
   331  			expectedFilterTree: []expectedParseNode{
   332  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   333  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   334  				{Value: "'abc'", Depth: 1, Type: ExpressionTokenString},
   335  			},
   336  			expectedOrderBy: []OrderByItem{
   337  				{Field: &Token{Value: "Title"}, Order: "asc"},
   338  			},
   339  		},
   340  		{
   341  			url:      "/Books?$filter=Description eq 'abc'&$orderby=Title asc",
   342  			errRegex: nil,
   343  			expectedFilterTree: []expectedParseNode{
   344  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   345  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   346  				{Value: "'abc'", Depth: 1, Type: ExpressionTokenString},
   347  			},
   348  			expectedOrderBy: []OrderByItem{
   349  				{Field: &Token{Value: "Title"}, Order: "asc"},
   350  			},
   351  		},
   352  		{
   353  			url:      "/Books?$filter=Description eq 'abc'&$orderby=Title desc",
   354  			errRegex: nil,
   355  			expectedFilterTree: []expectedParseNode{
   356  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   357  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   358  				{Value: "'abc'", Depth: 1, Type: ExpressionTokenString},
   359  			},
   360  			expectedOrderBy: []OrderByItem{
   361  				{Field: &Token{Value: "Title"}, Order: "desc"},
   362  			},
   363  		},
   364  		{
   365  			url:      "/Books?$filter=Description eq 'abc'&$orderby=Author asc,Title desc",
   366  			errRegex: nil,
   367  			expectedFilterTree: []expectedParseNode{
   368  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   369  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   370  				{Value: "'abc'", Depth: 1, Type: ExpressionTokenString},
   371  			},
   372  			expectedOrderBy: []OrderByItem{
   373  				{Field: &Token{Value: "Author"}, Order: "asc"},
   374  				{Field: &Token{Value: "Title"}, Order: "desc"},
   375  			},
   376  		},
   377  		{
   378  			url:      "/Books?$filter=Description eq 'abc'&$orderby=Author    asc,Title     DESC",
   379  			errRegex: nil,
   380  			expectedFilterTree: []expectedParseNode{
   381  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   382  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   383  				{Value: "'abc'", Depth: 1, Type: ExpressionTokenString},
   384  			},
   385  			expectedOrderBy: []OrderByItem{
   386  				{Field: &Token{Value: "Author"}, Order: "asc"},
   387  				{Field: &Token{Value: "Title"}, Order: "desc"},
   388  			},
   389  		},
   390  		{
   391  			url:                "/Products?$orderby=Asc",
   392  			errRegex:           nil,
   393  			expectedFilterTree: nil,
   394  			expectedOrderBy: []OrderByItem{
   395  				{Field: &Token{Value: "Asc"}, Order: "asc"},
   396  			},
   397  		},
   398  		{
   399  			url:                "/Products?$orderby=Asc Asc",
   400  			errRegex:           nil,
   401  			expectedFilterTree: nil,
   402  			expectedOrderBy: []OrderByItem{
   403  				{Field: &Token{Value: "Asc"}, Order: "asc"},
   404  			},
   405  		},
   406  		{
   407  			url:                "/Products?$orderby=Desc Asc",
   408  			errRegex:           nil,
   409  			expectedFilterTree: nil,
   410  			expectedOrderBy: []OrderByItem{
   411  				{Field: &Token{Value: "Desc"}, Order: "asc"},
   412  			},
   413  		},
   414  		{
   415  			url:                "/Products?$orderby=Asc Desc",
   416  			errRegex:           nil,
   417  			expectedFilterTree: nil,
   418  			expectedOrderBy: []OrderByItem{
   419  				{Field: &Token{Value: "Asc"}, Order: "desc"},
   420  			},
   421  		},
   422  		{
   423  			url:                "/Products?$orderby=ProductDesc",
   424  			errRegex:           nil,
   425  			expectedFilterTree: nil,
   426  			expectedOrderBy: []OrderByItem{
   427  				{Field: &Token{Value: "ProductDesc"}, Order: "asc"},
   428  			},
   429  		},
   430  
   431  		/*
   432  			TODO: this is not supported yet.
   433  			{
   434  				// return all Categories ordered by the number of Products within each category.
   435  				url:                "Categories?$orderby=Products/$count",
   436  				errRegex:           nil,
   437  				expectedFilterTree: nil,
   438  				expectedOrderBy: []OrderByItem{
   439  					{
   440  						Field: &Token{Value: "Products/$count"},
   441  						Order: "asc",
   442  					},
   443  				},
   444  			},
   445  		*/
   446  		{
   447  			url:      "/Product?$filter=Description eq 'abc'&$orderby=part_x0020_number asc",
   448  			errRegex: nil,
   449  			expectedFilterTree: []expectedParseNode{
   450  				{Value: "eq", Depth: 0, Type: ExpressionTokenLogical},
   451  				{Value: "Description", Depth: 1, Type: ExpressionTokenLiteral},
   452  				{Value: "'abc'", Depth: 1, Type: ExpressionTokenString},
   453  			},
   454  			expectedOrderBy: []OrderByItem{
   455  				{
   456  					Field: &Token{Value: "part number"},
   457  					Order: "asc",
   458  				},
   459  			},
   460  		},
   461  		{
   462  			url:                "/Product?$orderby=Tags(Key='Environment')/Value desc",
   463  			errRegex:           nil,
   464  			expectedFilterTree: nil,
   465  			expectedOrderBy: []OrderByItem{
   466  				{
   467  					Field: &Token{Value: "Tags(Key='Environment')/Value"},
   468  					Order: "desc",
   469  				},
   470  			},
   471  		},
   472  		{
   473  			url:                "/Product?$orderby=Tags(Key='Sku Number')/Value",
   474  			errRegex:           nil,
   475  			expectedFilterTree: nil,
   476  			expectedOrderBy: []OrderByItem{
   477  				{
   478  					Field: &Token{Value: "Tags(Key='Sku Number')/Value"},
   479  					Order: "asc",
   480  				},
   481  			},
   482  		},
   483  		{
   484  			// Disallow $orderby=+Name
   485  			// Query string uses %2B which is the escape for +. The + character is itself a url escape for space, see https://www.w3schools.com/tags/ref_urlencode.asp.
   486  			url:                "/Product?$orderby=%2BName",
   487  			errRegex:           regexp.MustCompile(`.*Token '\+Name' is invalid.*`),
   488  			expectedFilterTree: nil,
   489  			expectedOrderBy:    nil,
   490  		},
   491  		{
   492  			url:                "/Product?$orderby=-Name",
   493  			errRegex:           regexp.MustCompile(".*Token '-Name' is invalid.*"),
   494  			expectedFilterTree: nil,
   495  			expectedOrderBy:    nil,
   496  		},
   497  		{
   498  			url:                "/Product?$orderby=Name,",
   499  			errRegex:           regexp.MustCompile(`Extra comma in \$orderby\.`),
   500  			expectedFilterTree: nil,
   501  			expectedOrderBy:    nil,
   502  		},
   503  		{
   504  			url:                "/Product?$orderby=Name,,Count",
   505  			errRegex:           regexp.MustCompile(`Extra comma in \$orderby\.`),
   506  			expectedFilterTree: nil,
   507  			expectedOrderBy:    nil,
   508  		},
   509  		{
   510  			url:                "/Product?$orderby=,Name",
   511  			errRegex:           regexp.MustCompile(`Extra comma in \$orderby\.`),
   512  			expectedFilterTree: nil,
   513  			expectedOrderBy:    nil,
   514  		},
   515  		{
   516  			url:                "/Product?$select=Name,",
   517  			errRegex:           regexp.MustCompile(`Extra comma in \$select\.`),
   518  			expectedFilterTree: nil,
   519  			expectedOrderBy:    nil,
   520  		},
   521  		{
   522  			url:                "/Product?$select=Name,,Count",
   523  			errRegex:           regexp.MustCompile(`Extra comma in \$select\.`),
   524  			expectedFilterTree: nil,
   525  			expectedOrderBy:    nil,
   526  		},
   527  		{
   528  			url:                "/Product?$select=,Name",
   529  			errRegex:           regexp.MustCompile(`Extra comma in \$select\.`),
   530  			expectedFilterTree: nil,
   531  			expectedOrderBy:    nil,
   532  		},
   533  		{
   534  			url:                "/Product?$select=$select=Name",
   535  			errRegex:           regexp.MustCompile(`Invalid \$select\ value.`),
   536  			expectedFilterTree: nil,
   537  			expectedOrderBy:    nil,
   538  		},
   539  		{
   540  			url:                "/Product?$select=$Name",
   541  			errRegex:           regexp.MustCompile(`Invalid \$select\ value.`),
   542  			expectedFilterTree: nil,
   543  			expectedOrderBy:    nil,
   544  		},
   545  		{
   546  			url:      "/Product?$compute=Price mul Quantity as TotalPrice",
   547  			errRegex: nil,
   548  			expectedCompute: []ComputeItem{
   549  				{
   550  					Field: "TotalPrice",
   551  				},
   552  			},
   553  		},
   554  		{
   555  			url:      "/Product?$compute=Price mul Quantity as Extra/TotalPrice",
   556  			errRegex: nil,
   557  			expectedCompute: []ComputeItem{
   558  				{
   559  					Field: "Extra/TotalPrice",
   560  				},
   561  			},
   562  		},
   563  		{
   564  			url: "/Product?$compute=Price mul Quantity as TotalPrice,A add B as C",
   565  			expectedCompute: []ComputeItem{
   566  				{
   567  					Field: "TotalPrice",
   568  				},
   569  				{
   570  					Field: "C",
   571  				},
   572  			},
   573  		},
   574  		{
   575  			url: "/Product?$expand=Details($compute=Price mul Quantity as TotalPrice)",
   576  			// todo: enhance fixture to handle $expand with embedded $compute and add assertions
   577  		},
   578  		{
   579  			url: "/Product?$compute=discount(Item/Price) as SalePrice",
   580  			expectedCompute: []ComputeItem{
   581  				{
   582  					Field: "SalePrice",
   583  				},
   584  			},
   585  		},
   586  		{
   587  			url:      "/Product?$compute=Price mul Quantity",
   588  			errRegex: regexp.MustCompile(`Invalid \$compute query option`),
   589  		},
   590  		{
   591  			url:      "/Product?$compute=Price bad Quantity as TotalPrice",
   592  			errRegex: regexp.MustCompile(`Invalid \$compute query option`),
   593  		},
   594  		{
   595  			url:      "/Product?$compute=Price mul Quantity as as TotalPrice",
   596  			errRegex: regexp.MustCompile(`Invalid \$compute query option`),
   597  		},
   598  		{
   599  			url:      "/Product?$compute=Price mul Quantity as TotalPrice as TotalPrice2",
   600  			errRegex: regexp.MustCompile(`Invalid \$compute query option`),
   601  		},
   602  		{
   603  			url:      "/Product?$compute=TotalPrice as Price mul Quantity",
   604  			errRegex: regexp.MustCompile(`Invalid \$compute query option`),
   605  		},
   606  	}
   607  	err := DefineCustomFunctions([]CustomFunctionInput{{
   608  		Name:      "discount",
   609  		NumParams: []int{1},
   610  	}})
   611  	if err != nil {
   612  		t.Errorf("Failed to add custom function: %v", err)
   613  		t.FailNow()
   614  	}
   615  
   616  	for _, testCase := range testCases {
   617  		var parsedUrl *url.URL
   618  		parsedUrl, err = url.Parse(testCase.url)
   619  		if err != nil {
   620  			t.Errorf("Test case '%s' failed: %v", testCase.url, err)
   621  			continue
   622  		}
   623  		t.Logf("Running test case %s", testCase.url)
   624  
   625  		urlQuery := parsedUrl.Query()
   626  		ctx := context.Background()
   627  		var request *GoDataRequest
   628  		request, err = ParseRequest(ctx, parsedUrl.Path, urlQuery)
   629  		if testCase.errRegex == nil && err != nil {
   630  			t.Errorf("Test case '%s' failed: %v", testCase.url, err)
   631  			continue
   632  		} else if testCase.errRegex != nil && err == nil {
   633  			t.Errorf("Test case '%s' failed. Expected error but obtained nil error", testCase.url)
   634  			continue
   635  		} else if err != nil && !testCase.errRegex.MatchString(err.Error()) {
   636  			t.Errorf("Test case '%s' failed. Obtained error [%v] does not match expected regex [%v]",
   637  				testCase.url, err, testCase.errRegex)
   638  			continue
   639  		}
   640  		if err == nil {
   641  			filter := request.Query.Filter
   642  			if filter == nil {
   643  				if testCase.expectedFilterTree != nil {
   644  					t.Errorf("Test case '%s' failed. Parsed filter is nil", testCase.url)
   645  				}
   646  			} else {
   647  				pos := 0
   648  				err = CompareTree(filter.Tree, testCase.expectedFilterTree, &pos, 0)
   649  				if err != nil {
   650  					t.Errorf("Tree representation does not match expected value. error: %s", err.Error())
   651  				}
   652  			}
   653  
   654  			err = compareOrderBy(request.Query.OrderBy, testCase.expectedOrderBy)
   655  			if err != nil {
   656  				t.Errorf("orderby does not match expected value. error: %s", err.Error())
   657  			}
   658  
   659  			err = compareCompute(request.Query.Compute, testCase.expectedCompute)
   660  			if err != nil {
   661  				t.Errorf("compute does not match expected value. error: %s", err.Error())
   662  			}
   663  			//			t.Log(request.Query.Compute.ComputeItems[0])
   664  		}
   665  	}
   666  }
   667  
   668  func compareOrderBy(obtained *GoDataOrderByQuery, expected []OrderByItem) error {
   669  	if len(expected) == 0 && (obtained == nil || obtained.OrderByItems == nil) {
   670  		return nil
   671  	}
   672  	if len(expected) > 0 && (obtained == nil || obtained.OrderByItems == nil) {
   673  		return fmt.Errorf("Unexpected number of $orderby fields. Got nil, expected %d",
   674  			len(expected))
   675  	}
   676  	if len(obtained.OrderByItems) != len(expected) {
   677  		return fmt.Errorf("Unexpected number of $orderby fields. Got %d, expected %d",
   678  			len(obtained.OrderByItems), len(expected))
   679  	}
   680  	for i, v := range expected {
   681  		if v.Field.Value != obtained.OrderByItems[i].Field.Value {
   682  			return fmt.Errorf("Unexpected $orderby field at index %d. Got '%s', expected '%s'",
   683  				i, obtained.OrderByItems[i].Field.Value, v.Field.Value)
   684  		}
   685  		if v.Order != obtained.OrderByItems[i].Order {
   686  			return fmt.Errorf("Unexpected $orderby at index %d. Got '%s', expected '%s'",
   687  				i, obtained.OrderByItems[i].Order, v.Order)
   688  		}
   689  	}
   690  	return nil
   691  }
   692  
   693  func compareCompute(obtained *GoDataComputeQuery, expected []ComputeItem) error {
   694  	if len(expected) == 0 && (obtained == nil || obtained.ComputeItems == nil) {
   695  		return nil
   696  	}
   697  	if len(expected) > 0 && (obtained == nil || obtained.ComputeItems == nil) {
   698  		return fmt.Errorf("Unexpected number of $compute fields. Got nil, expected %d",
   699  			len(expected))
   700  	}
   701  	if len(obtained.ComputeItems) != len(expected) {
   702  		return fmt.Errorf("Unexpected number of $compute fields. Got %d, expected %d",
   703  			len(obtained.ComputeItems), len(expected))
   704  	}
   705  	for i, v := range expected {
   706  		if obtained.ComputeItems[i].Field != v.Field {
   707  			return fmt.Errorf("Expected $compute field %d with name '%s'. Got '%s'.", i, v.Field, obtained.ComputeItems[i].Field)
   708  		}
   709  	}
   710  	return nil
   711  }