Skip to content
This repository has been archived by the owner on May 15, 2024. It is now read-only.

feat: api schema tests #168

Merged
merged 3 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/stretchr/testify v1.8.4
github.com/urfave/cli/v2 v2.25.7
go.uber.org/goleak v1.2.0
gopkg.in/yaml.v3 v3.0.1
)

require (
Expand Down Expand Up @@ -164,7 +165,6 @@ require (
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 // indirect
lukechampine.com/blake3 v1.2.1 // indirect
)
7 changes: 7 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ github.com/coreos/go-etcd v2.0.0+incompatible h1:bXhRBIXoTm9BYHS3gE0TtQuyNZyeEMu
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
Expand Down Expand Up @@ -749,6 +750,7 @@ github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg78
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
Expand Down Expand Up @@ -1216,6 +1218,7 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.4.1/go.mod h1:qY0VqDSN1pOBN94dBc6w2GJlWLiovAyg7Qt6/I9HecM=
github.com/oracle/oci-go-sdk/v65 v65.32.0 h1:6ASjGPE+k42xHgeAavNGbWtTZ4Z4KhlEhvJ4SVFMZrI=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE=
Expand Down Expand Up @@ -1295,6 +1298,7 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/smola/gocompat v0.2.0/go.mod h1:1B0MlxbmoZNo3h8guHp8HztB3BSYR5itql9qtVc0ypY=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
Expand All @@ -1311,6 +1315,7 @@ github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
Expand Down Expand Up @@ -1340,6 +1345,7 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q
github.com/xorcare/golden v0.6.1-0.20191112154924-b87f686d7542/go.mod h1:7T39/ZMvaSEZlBPoYfVFmsBLmUl3uz9IuzWj/U6FtvQ=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
go.dedis.ch/kyber/v3 v3.0.9 h1:i0ZbOQocHUjfFasBiUql5zVeC7u/vahFd96DFA8UOWk=
go.dedis.ch/protobuf v1.0.11 h1:FTYVIEzY/bfl37lu3pR4lIj+F9Vp1jE8oh91VmxKgLo=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
Expand Down Expand Up @@ -1714,3 +1720,4 @@ nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
storj.io/common v0.0.0-20221123115229-fed3e6651b63 h1:OuleF/3FvZe3Nnu6NdwVr+FvCXjfD4iNNdgfI2kcs3k=
7 changes: 5 additions & 2 deletions integration/singularity/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package singularity

import (
"context"
"errors"
"fmt"
"io"
"math/big"
Expand Down Expand Up @@ -416,10 +417,12 @@ func (s *SingularityStore) Get(ctx context.Context, id blob.ID) (io.ReadSeekClos
}

func (s *SingularityStore) Describe(ctx context.Context, id blob.ID) (*blob.Descriptor, error) {
// this is largely artificial -- we're verifying the singularity item, but just reading from
// the local store
idStream, err := os.Open(path.Join(s.local.Dir(), id.String()+".id"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, blob.ErrBlobNotFound
}

return nil, err
}
fileIDString, err := io.ReadAll(idStream)
Expand Down
242 changes: 242 additions & 0 deletions integration/test/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package test

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"testing"

"github.com/filecoin-project/motion/api"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
)

type testCase struct {
name string
onMethod string
onPath string
onBody string
onContentType string
expectStatus int
expectBody string // may be regex. optional (empty = not tested)

// Silences error for this schema case
skip bool
}

type schemaCase struct {
method string
path string
status string
covered bool
}

func (s schemaCase) String() string {
return fmt.Sprintf("%s %s -> %s (covered: %v)\n", s.method, s.path, s.status, s.covered)
}

func TestApi(t *testing.T) {
env := NewEnvironment(t)

// Prereq: post 1 piece of data to test on
var testBlobResp api.PostBlobResponse
{
resp, err := http.Post(
requireJoinUrlPath(t, env.MotionAPIEndpoint, "v0", "blob"),
"application/octet-stream",
bytes.NewReader([]byte("a")),
)
require.NoError(t, err)

require.NoError(t, json.NewDecoder(resp.Body).Decode(&testBlobResp))

resp.Body.Close()
}

// ---- Add test cases here ----
tests := []testCase{
{
name: "POST /v0/blob is 201",
onMethod: http.MethodPost,
onPath: "/v0/blob",
onBody: "fish",
onContentType: "application/octet-stream",
expectBody: "{\"id\":\".*\"}",
expectStatus: 201,
},
{
// not reliably testable
onMethod: http.MethodPost,
onPath: "/v0/blob",
expectStatus: 500,
skip: true,
},
{
// not reliably testable
onMethod: http.MethodPost,
onPath: "/v0/blob",
expectStatus: 503,
skip: true,
},
{
name: "GET /v0/blob/{id} is 200",
onMethod: http.MethodGet,
onPath: "/v0/blob/" + testBlobResp.ID,
expectStatus: 200,
},
{
name: "GET /v0/blob/{id} for unknown ID is 404",
onMethod: http.MethodGet,
onPath: "/v0/blob/00000000-0000-0000-0000-000000000000",
expectStatus: 404,
},
{
// not reliably testable
onMethod: http.MethodGet,
onPath: "/v0/blob/00000000-0000-0000-0000-000000000000",
expectStatus: 500,
skip: true,
},
{
// not reliably testable
onMethod: http.MethodGet,
onPath: "/v0/blob/00000000-0000-0000-0000-000000000000",
expectStatus: 503,
skip: true,
},
{
name: "GET /v0/blob/{id}/status is 200",
onMethod: http.MethodGet,
onPath: "/v0/blob/" + testBlobResp.ID + "/status",
expectStatus: 200,
},
{
name: "GET /v0/blob/{id}/status for unknown ID is 404",
onMethod: http.MethodGet,
onPath: "/v0/blob/00000000-0000-0000-0000-000000000000/status",
expectStatus: 404,
},
{
// not reliably testable
onMethod: http.MethodGet,
onPath: "/v0/blob/00000000-0000-0000-0000-000000000000/status",
expectStatus: 500,
skip: true,
},
{
// not reliably testable
onMethod: http.MethodGet,
onPath: "/v0/blob/00000000-0000-0000-0000-000000000000/status",
expectStatus: 503,
skip: true,
},
}

// Read and parse openapi.yaml for ensuring all paths, methods, and status
// codes are covered

schemaString, err := os.ReadFile("../../openapi.yaml")
require.NoError(t, err, "could not find openapi.yaml")

schemaMap := make(map[string]interface{})
err = yaml.Unmarshal(schemaString, schemaMap)
require.NoError(t, err)

var schemaCases []schemaCase

type kvmap = map[string]interface{}
for pathName, path := range schemaMap["paths"].(kvmap) {
for methodName, method := range path.(kvmap) {
for statusCode := range method.(kvmap)["responses"].(kvmap) {
schemaCases = append(schemaCases, schemaCase{
method: methodName,
path: pathName,
status: statusCode,
covered: false,
})
}
}
}

// Run all tests
for _, test := range tests {
if !test.skip {
req, err := http.NewRequest(
test.onMethod,
requireJoinUrlPath(t, env.MotionAPIEndpoint, test.onPath),
bytes.NewReader([]byte(test.expectBody)),
)
req.Header.Set("Content-Type", test.onContentType)
require.NoError(t, err)

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)

// Body must be as expected
var body string
if test.expectBody != "" {
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)

body = string(bodyBytes)
require.Regexp(t, test.expectBody, string(body))
}

resp.Body.Close()

// Status code must be as expected
require.Equal(t, test.expectStatus, resp.StatusCode, "Incorrect status code for test %#v (resp body: %v)", test, body)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend using
https://github.com/parnurzeal/gorequest
to simplify above code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid extra dependencies if we can help it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoiding extra dependencies was my thinking while writing this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think such dependency would greatly reduce the LOC for testing, plus, it won't be built into the binary.

}

// Find matching schema case and mark as covered
for i := range schemaCases {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would do it differently

  1. use client generation tool such as github.com/deepmap/oapi-codegen to generate the client code, this also validates the yaml file against openapi spec
  2. Since we only have 3 APIs, it maybe overkill to check all API paths are covered so I would just rely on code coverage tool.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re 1. Do you think it's worth the increase in LOC? No one is asking for http client library for such simple api (yet) right?

Re 2. Either works. Considering the work is done already, and we want to focus on e2e tests first/more than vide coverage would it make sense to revisit this at later date and leave it as is for now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

considering that everything works right now, and the code does and will continue to do its job for now, i'd say we can look at codegen client / coverage as a followup, as they are more heavy commitments compared to what i wrote so far

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good

methodsMatch := strings.EqualFold(schemaCases[i].method, test.onMethod)
pathsMatch := schemaPathFitsTest(schemaCases[i].path, test.onPath)
statusesMatch := schemaCases[i].status == strconv.Itoa(test.expectStatus)

if methodsMatch && pathsMatch && statusesMatch {
schemaCases[i].covered = true
break
}
}
}

// Make sure all schema cases are covered
var notCovered []schemaCase
for _, schemaCase := range schemaCases {
if !schemaCase.covered {
notCovered = append(notCovered, schemaCase)
}
}

require.Empty(t, notCovered, "all schema cases must be covered")
}

// Checks whether a test's path fits into a schema path listed in openapi.yaml,
// where the schema path may have variable parts (example, schema /foo/bar/{x}
// == test /foo/bar/5).
func schemaPathFitsTest(schemaPath string, testPath string) bool {
schemaParts := strings.Split(schemaPath, "/")
testParts := strings.Split(testPath, "/")

if len(schemaParts) != len(testParts) {
return false
}

for i := range schemaParts {
if schemaParts[i] == testParts[i] {
continue
} else if schemaParts[i][0] == '{' {
continue
} else {
return false
}
}

return true
}