refactor: flatten and separate (#353)

* refactor(atomicx): move outside the engine package

After merging probe-engine into probe-cli, my impression is that we have
too much unnecessary nesting of packages in this repository.

The idea of this commit and of a bunch of following commits will instead
be to reduce the nesting and simplify the structure.

While there, improve the documentation.

* fix: always use the atomicx package

For consistency, never use sync/atomic and always use ./internal/atomicx
so we can just grep and make sure we're not risking to crash if we make
a subtle mistake on a 32 bit platform.

While there, mention in the contributing guidelines that we want to
always prefer the ./internal/atomicx package over sync/atomic.

* fix(atomicx): remove unnecessary constructor

We don't need a constructor here. The default constructed `&Int64{}`
instance is already usable and the constructor does not add anything to
what we are doing, rather it just creates extra confusion.

* cleanup(atomicx): we are not using Float64

Because atomicx.Float64 is unused, we can safely zap it.

* cleanup(atomicx): simplify impl and improve tests

We can simplify the implementation by using defer and by letting
the Load() method call Add(0).

We can improve tests by making many goroutines updated the
atomic int64 value concurrently.

* refactor(fsx): can live in the ./internal pkg

Let us reduce the amount of nesting. While there, ensure that the
package only exports the bare minimum, and improve the documentation
of the tests, to ease reading the code.

* refactor: move runtimex to ./internal

* refactor: move shellx into the ./internal package

While there, remove unnecessary dependency between packages.

While there, specify in the contributing guidelines that
one should use x/sys/execabs instead of os/exec.

* refactor: move ooapi into the ./internal pkg

* refactor(humanize): move to ./internal and better docs

* refactor: move platform to ./internal

* refactor(randx): move to ./internal

* refactor(multierror): move into the ./internal pkg

* refactor(kvstore): all kvstores in ./internal

Rather than having part of the kvstore inside ./internal/engine/kvstore
and part in ./internal/engine/kvstore.go, let us put every piece of code
that is kvstore related into the ./internal/kvstore package.

* fix(kvstore): always return ErrNoSuchKey on Get() error

It should help to use the kvstore everywhere removing all the
copies that are lingering around the tree.

* sessionresolver: make KVStore mandatory

Simplifies implementation. While there, use the ./internal/kvstore
package rather than having our private implementation.

* fix(ooapi): use the ./internal/kvstore package

* fix(platform): better documentation
This commit is contained in:
Simone Basso 2021-06-04 10:34:18 +02:00 committed by GitHub
commit 33de701263
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
169 changed files with 1136 additions and 1003 deletions

View file

@ -0,0 +1,180 @@
package main
import (
"fmt"
"strings"
"time"
)
// apiField contains the fields of an API data structure
type apiField struct {
// name is the field name
name string
// kind is the filed type
kind string
// comment is a brief comment to document the field
comment string
// ifLogin indicates whether this field should only be
// emitted when the API requires login
ifLogin bool
// ifTemplate indicates whether this field should only be
// emitted when the URL path is a template
ifTemplate bool
// noClone is true when this field should not be copied
// from the parent data structure when cloning
noClone bool
}
var apiFields = []apiField{{
name: "BaseURL",
kind: "string",
comment: "optional",
}, {
name: "HTTPClient",
kind: "HTTPClient",
comment: "optional",
}, {
name: "JSONCodec",
kind: "JSONCodec",
comment: "optional",
}, {
name: "Token",
kind: "string",
comment: "mandatory",
ifLogin: true,
noClone: true,
}, {
name: "RequestMaker",
kind: "RequestMaker",
comment: "optional",
}, {
name: "TemplateExecutor",
kind: "templateExecutor",
comment: "optional",
ifTemplate: true,
}, {
name: "UserAgent",
kind: "string",
comment: "optional",
}}
func (d *Descriptor) genNewAPI(sb *strings.Builder) {
fmt.Fprintf(sb, "// %s implements the %s API.\n", d.APIStructName(), d.Name)
fmt.Fprintf(sb, "type %s struct {\n", d.APIStructName())
for _, f := range apiFields {
if !d.RequiresLogin && f.ifLogin {
continue
}
if !d.URLPath.IsTemplate && f.ifTemplate {
continue
}
fmt.Fprintf(sb, "\t%s %s // %s\n", f.name, f.kind, f.comment)
}
fmt.Fprint(sb, "}\n\n")
if d.RequiresLogin {
fmt.Fprintf(sb, "// WithToken returns a copy of the API where the\n")
fmt.Fprintf(sb, "// value of the Token field is replaced with token.\n")
fmt.Fprintf(sb, "func (api *%s) WithToken(token string) %s {\n",
d.APIStructName(), d.CallerInterfaceName())
fmt.Fprintf(sb, "out := &%s{}\n", d.APIStructName())
for _, f := range apiFields {
if !d.URLPath.IsTemplate && f.ifTemplate {
continue
}
if f.noClone == true {
continue
}
fmt.Fprintf(sb, "out.%s = api.%s\n", f.name, f.name)
}
fmt.Fprint(sb, "out.Token = token\n")
fmt.Fprint(sb, "return out\n")
fmt.Fprint(sb, "}\n\n")
}
fmt.Fprintf(sb, "func (api *%s) baseURL() string {\n", d.APIStructName())
fmt.Fprint(sb, "\tif api.BaseURL != \"\" {\n")
fmt.Fprint(sb, "\t\treturn api.BaseURL\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn \"https://ps1.ooni.io\"\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) requestMaker() RequestMaker {\n", d.APIStructName())
fmt.Fprint(sb, "\tif api.RequestMaker != nil {\n")
fmt.Fprint(sb, "\t\treturn api.RequestMaker\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &defaultRequestMaker{}\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) jsonCodec() JSONCodec {\n", d.APIStructName())
fmt.Fprint(sb, "\tif api.JSONCodec != nil {\n")
fmt.Fprint(sb, "\t\treturn api.JSONCodec\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &defaultJSONCodec{}\n")
fmt.Fprint(sb, "}\n\n")
if d.URLPath.IsTemplate {
fmt.Fprintf(
sb, "func (api *%s) templateExecutor() templateExecutor {\n",
d.APIStructName())
fmt.Fprint(sb, "\tif api.TemplateExecutor != nil {\n")
fmt.Fprint(sb, "\t\treturn api.TemplateExecutor\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &defaultTemplateExecutor{}\n")
fmt.Fprint(sb, "}\n\n")
}
fmt.Fprintf(
sb, "func (api *%s) httpClient() HTTPClient {\n",
d.APIStructName())
fmt.Fprint(sb, "\tif api.HTTPClient != nil {\n")
fmt.Fprint(sb, "\t\treturn api.HTTPClient\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn http.DefaultClient\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "// Call calls the %s API.\n", d.Name)
fmt.Fprintf(
sb, "func (api *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
d.APIStructName(), d.RequestTypeName(), d.ResponseTypeName())
fmt.Fprint(sb, "\thttpReq, err := api.newRequest(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\thttpReq.Header.Add(\"Accept\", \"application/json\")\n")
if d.RequiresLogin {
fmt.Fprint(sb, "\tif api.Token == \"\" {\n")
fmt.Fprint(sb, "\t\treturn nil, ErrMissingToken\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\thttpReq.Header.Add(\"Authorization\", newAuthorizationHeader(api.Token))\n")
}
fmt.Fprint(sb, "\tif api.UserAgent != \"\" {\n")
fmt.Fprint(sb, "\t\thttpReq.Header.Add(\"User-Agent\", api.UserAgent)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn api.newResponse(api.httpClient().Do(httpReq))\n")
fmt.Fprint(sb, "}\n\n")
}
// GenAPIsGo generates apis.go.
func GenAPIsGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"net/http\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
desc.genNewAPI(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,461 @@
package main
import (
"fmt"
"reflect"
"strings"
"time"
)
func (d *Descriptor) genTestNewRequest(sb *strings.Builder) {
fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\tff.fill(req)\n")
}
func (d *Descriptor) genTestInvalidURL(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sInvalidURL(t *testing.T) {\n", d.Name)
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tBaseURL: \"\\t\", // invalid\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err == nil || !strings.HasSuffix(err.Error(), \"invalid control character in URL\") {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWithMissingToken(sb *strings.Builder) {
if d.RequiresLogin == false {
return // does not make sense when login isn't required
}
fmt.Fprintf(sb, "func Test%sWithMissingToken(t *testing.T) {\n", d.Name)
fmt.Fprintf(sb, "\tapi := &%s{} // no token\n", d.APIStructName())
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, ErrMissingToken) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWithHTTPErr(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWithHTTPErr(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Err: errMocked}\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestMarshalErr(sb *strings.Builder) {
if d.Method != "POST" {
return // does not make sense when we don't send a request body
}
fmt.Fprintf(sb, "func Test%sMarshalErr(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{EncodeErr: errMocked},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWithNewRequestErr(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWithNewRequestErr(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tRequestMaker: &FakeRequestMaker{Err: errMocked},\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWith401(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWith401(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 401}}\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, ErrUnauthorized) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWith400(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWith400(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{StatusCode: 400}}\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWithResponseBodyReadErr(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWithResponseBodyReadErr(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
fmt.Fprint(sb, "\t\tStatusCode: 200,\n")
fmt.Fprint(sb, "\t\tBody: &FakeBody{Err: errMocked},\n")
fmt.Fprint(sb, "\t}}\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWithUnmarshalFailure(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWithUnmarshalFailure(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
fmt.Fprint(sb, "\t\tStatusCode: 200,\n")
fmt.Fprint(sb, "\t\tBody: &FakeBody{Data: []byte(`{}`)},\n")
fmt.Fprint(sb, "\t}}\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
fmt.Fprintf(sb, "\t\tJSONCodec: &FakeCodec{DecodeErr: errMocked},\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprintf(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestRoundTrip(sb *strings.Builder) {
// generate the type of the handler
fmt.Fprintf(sb, "type handle%s struct {\n", d.Name)
fmt.Fprint(sb, "\taccept string\n")
fmt.Fprint(sb, "\tbody []byte\n")
fmt.Fprint(sb, "\tcontentType string\n")
fmt.Fprint(sb, "\tcount int32\n")
fmt.Fprint(sb, "\tmethod string\n")
fmt.Fprint(sb, "\tmu sync.Mutex\n")
fmt.Fprintf(sb, "\tresp %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\turl *url.URL\n")
fmt.Fprint(sb, "\tuserAgent string\n")
fmt.Fprint(sb, "}\n\n")
// generate the handling function
fmt.Fprintf(sb,
"func (h *handle%s) ServeHTTP(w http.ResponseWriter, r *http.Request) {",
d.Name)
fmt.Fprint(sb, "\tdefer h.mu.Unlock()\n")
fmt.Fprint(sb, "\th.mu.Lock()\n")
fmt.Fprint(sb, "\tif h.count > 0 {\n")
fmt.Fprint(sb, "\t\tw.WriteHeader(400)\n")
fmt.Fprint(sb, "\t\treturn\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\th.count++\n")
fmt.Fprint(sb, "\tif r.Body != nil {\n")
fmt.Fprint(sb, "\t\tdata, err := ioutil.ReadAll(r.Body)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n")
fmt.Fprintf(sb, "\t\t\treturn\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\th.body = data\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\th.method = r.Method\n")
fmt.Fprint(sb, "\th.url = r.URL\n")
fmt.Fprint(sb, "\th.accept = r.Header.Get(\"Accept\")\n")
fmt.Fprint(sb, "\th.contentType = r.Header.Get(\"Content-Type\")\n")
fmt.Fprint(sb, "\th.userAgent = r.Header.Get(\"User-Agent\")\n")
fmt.Fprintf(sb, "\tvar out %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff := fakeFill{}\n")
fmt.Fprint(sb, "\tff.fill(&out)\n")
fmt.Fprintf(sb, "\th.resp = out\n")
fmt.Fprintf(sb, "\tdata, err := json.Marshal(out)\n")
fmt.Fprintf(sb, "\tif err != nil {\n")
fmt.Fprintf(sb, "\t\tw.WriteHeader(400)\n")
fmt.Fprintf(sb, "\t\treturn\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tw.Write(data)\n")
fmt.Fprintf(sb, "\t}\n\n")
// generate the test itself
fmt.Fprintf(sb, "func Test%sRoundTrip(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\t// setup\n")
fmt.Fprintf(sb, "\thandler := &handle%s{}\n", d.Name)
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprintf(sb, "\tapi := &%s{BaseURL: srvr.URL}\n", d.APIStructName())
fmt.Fprint(sb, "\tff.fill(&api.UserAgent)\n")
if d.RequiresLogin {
fmt.Fprint(sb, "\tff.fill(&api.Token)\n")
}
fmt.Fprint(sb, "\t// issue request\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response here\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// compare our response and server's one\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.resp, resp); diff != \"\" {")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// check whether headers are OK\n")
fmt.Fprint(sb, "\tif handler.accept != \"application/json\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid accept header\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.userAgent != api.UserAgent {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid user-agent header\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// check whether the method is OK\n")
fmt.Fprintf(sb, "\tif handler.method != \"%s\" {\n", d.Method)
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid method\")\n")
fmt.Fprint(sb, "\t}\n")
if d.Method == "POST" {
fmt.Fprint(sb, "\t// check the body\n")
fmt.Fprint(sb, "\tif handler.contentType != \"application/json\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid content-type header\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tgot := &%s{}\n", d.RequestTypeNameAsStruct())
fmt.Fprintf(sb, "\tif err := json.Unmarshal(handler.body, &got); err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(req, got); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
} else {
fmt.Fprint(sb, "\t// check the query\n")
fmt.Fprint(sb, "\thttpReq, err := api.newRequest(context.Background(), req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
}
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestResponseLiteralNull(sb *strings.Builder) {
switch d.ResponseTypeKind() {
case reflect.Map:
// fallthrough
case reflect.Struct:
return // test not applicable
}
fmt.Fprintf(sb, "func Test%sResponseLiteralNull(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
fmt.Fprint(sb, "\t\tStatusCode: 200,\n")
fmt.Fprint(sb, "\t\tBody: &FakeBody{Data: []byte(`null`)},\n")
fmt.Fprint(sb, "\t}}\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, ErrJSONLiteralNull) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestMandatoryFields(sb *strings.Builder) {
fields := d.StructFieldsWithTag(d.Request, tagForRequired)
if len(fields) < 1 {
return // nothing to test
}
fmt.Fprintf(sb, "func Test%sMandatoryFields(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
fmt.Fprint(sb, "\t\tStatusCode: 500,\n")
fmt.Fprint(sb, "\t}}\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprintf(sb, "\treq := &%s{} // deliberately empty\n", d.RequestTypeNameAsStruct())
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, ErrEmptyField) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestTemplateErr(sb *strings.Builder) {
if !d.URLPath.IsTemplate {
return // nothing to test
}
fmt.Fprintf(sb, "func Test%sTemplateErr(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tclnt := &FakeHTTPClient{Resp: &http.Response{\n")
fmt.Fprint(sb, "\t\tStatusCode: 500,\n")
fmt.Fprint(sb, "\t}}\n")
fmt.Fprintf(sb, "\tapi := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: clnt,\n")
if d.RequiresLogin == true {
fmt.Fprint(sb, "\t\tToken: \"fakeToken\",\n")
}
fmt.Fprint(sb, "\t\tTemplateExecutor: &FakeTemplateExecutor{Err: errMocked},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
d.genTestNewRequest(sb)
fmt.Fprint(sb, "\tresp, err := api.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil resp\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
// TODO(bassosimone): we should add a panic for every switch for
// the type of a request or a response for robustness.
func (d *Descriptor) genAPITests(sb *strings.Builder) {
d.genTestInvalidURL(sb)
d.genTestWithMissingToken(sb)
d.genTestWithHTTPErr(sb)
d.genTestMarshalErr(sb)
d.genTestWithNewRequestErr(sb)
d.genTestWith401(sb)
d.genTestWith400(sb)
d.genTestWithResponseBodyReadErr(sb)
d.genTestWithUnmarshalFailure(sb)
d.genTestRoundTrip(sb)
d.genTestResponseLiteralNull(sb)
d.genTestMandatoryFields(sb)
d.genTestTemplateErr(sb)
}
// GenAPIsTestGo generates apis_test.go.
func GenAPIsTestGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"encoding/json\"\n")
fmt.Fprint(&sb, "\t\"errors\"\n")
fmt.Fprint(&sb, "\t\"io/ioutil\"\n")
fmt.Fprint(&sb, "\t\"net/http/httptest\"\n")
fmt.Fprint(&sb, "\t\"net/http\"\n")
fmt.Fprint(&sb, "\t\"net/url\"\n")
fmt.Fprint(&sb, "\t\"strings\"\n")
fmt.Fprint(&sb, "\t\"testing\"\n")
fmt.Fprint(&sb, "\t\"sync\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
desc.genAPITests(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,130 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) genNewCache(sb *strings.Builder) {
fmt.Fprintf(sb, "// %s implements caching for %s.\n",
d.WithCacheAPIStructName(), d.APIStructName())
fmt.Fprintf(sb, "type %s struct {\n", d.WithCacheAPIStructName())
fmt.Fprintf(sb, "\tAPI %s // mandatory\n", d.CallerInterfaceName())
fmt.Fprint(sb, "\tGobCodec GobCodec // optional\n")
fmt.Fprint(sb, "\tKVStore KVStore // mandatory\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "type %s struct {\n", d.CacheEntryName())
fmt.Fprintf(sb, "\tReq %s\n", d.RequestTypeName())
fmt.Fprintf(sb, "\tResp %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "// Call calls the API and implements caching.\n")
fmt.Fprintf(sb, "func (c *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
if d.CachePolicy == CacheAlways {
fmt.Fprint(sb, "\tif resp, _ := c.readcache(req); resp != nil {\n")
fmt.Fprint(sb, "\t\treturn resp, nil\n")
fmt.Fprint(sb, "\t}\n")
}
fmt.Fprint(sb, "\tresp, err := c.API.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
if d.CachePolicy == CacheFallback {
fmt.Fprint(sb, "\t\tif resp, _ := c.readcache(req); resp != nil {\n")
fmt.Fprint(sb, "\t\t\treturn resp, nil\n")
fmt.Fprint(sb, "\t\t}\n")
}
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif err := c.writecache(req, resp); err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn resp, nil\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (c *%s) gobCodec() GobCodec {\n", d.WithCacheAPIStructName())
fmt.Fprint(sb, "\tif c.GobCodec != nil {\n")
fmt.Fprint(sb, "\t\treturn c.GobCodec\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &defaultGobCodec{}\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (c *%s) getcache() ([]%s, error) {\n",
d.WithCacheAPIStructName(), d.CacheEntryName())
fmt.Fprintf(sb, "\tdata, err := c.KVStore.Get(\"%s\")\n", d.CacheKey())
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar out []%s\n", d.CacheEntryName())
fmt.Fprint(sb, "\tif err := c.gobCodec().Decode(data, &out); err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn out, nil\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (c *%s) setcache(in []%s) error {\n",
d.WithCacheAPIStructName(), d.CacheEntryName())
fmt.Fprint(sb, "\tdata, err := c.gobCodec().Encode(in)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\treturn c.KVStore.Set(\"%s\", data)\n", d.CacheKey())
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (c *%s) readcache(req %s) (%s, error) {\n",
d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
fmt.Fprint(sb, "\tcache, err := c.getcache()\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tfor _, cur := range cache {\n")
fmt.Fprint(sb, "\t\tif reflect.DeepEqual(req, cur.Req) {\n")
fmt.Fprint(sb, "\t\t\treturn cur.Resp, nil\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn nil, errCacheNotFound\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (c *%s) writecache(req %s, resp %s) error {\n",
d.WithCacheAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
fmt.Fprint(sb, "\tcache, _ := c.getcache()\n")
fmt.Fprintf(sb, "\tout := []%s{{Req: req, Resp: resp}}\n", d.CacheEntryName())
fmt.Fprint(sb, "\tconst toomany = 64\n")
fmt.Fprint(sb, "\tfor idx, cur := range cache {\n")
fmt.Fprint(sb, "\t\tif reflect.DeepEqual(req, cur.Req) {\n")
fmt.Fprint(sb, "\t\t\tcontinue // we already updated the cache\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif idx > toomany {\n")
fmt.Fprint(sb, "\t\t\tbreak\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tout = append(out, cur)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn c.setcache(out)\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "var _ %s = &%s{}\n\n", d.CallerInterfaceName(),
d.WithCacheAPIStructName())
}
// GenCachingGo generates caching.go.
func GenCachingGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"reflect\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
if desc.CachePolicy == CacheNone {
continue
}
desc.genNewCache(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,275 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) genTestCacheSuccess(sb *strings.Builder) {
fmt.Fprintf(sb, "func TestCache%sSuccess(t *testing.T) {\n", d.APIStructName())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWriteCacheError(sb *strings.Builder) {
fmt.Fprintf(sb, "func TestCache%sWriteCacheError(t *testing.T) {\n", d.APIStructName())
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tKVStore: &FakeKVStore{SetError: errMocked},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestFailureWithNoCache(sb *strings.Builder) {
fmt.Fprintf(sb, "func TestCache%sFailureWithNoCache(t *testing.T) {\n", d.APIStructName())
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\tErr: errMocked,\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := cache.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestFailureWithPreviousCache(sb *strings.Builder) {
// This works for both caching policies.
fmt.Fprintf(sb, "func TestCache%sFailureWithPreviousCache(t *testing.T) {\n", d.APIStructName())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprintf(sb, "\tfakeapi := &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
fmt.Fprint(sb, "\t\tAPI: fakeapi,\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\t// first pass with no error at all\n")
fmt.Fprint(sb, "\t// use a separate scope to be sure we avoid mistakes\n")
fmt.Fprint(sb, "\t{\n")
fmt.Fprint(sb, "\t\tresp, err := cache.Call(ctx, req)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif resp == nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// second pass with failure\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tfakeapi.Err = errMocked\n")
fmt.Fprint(sb, "\tfakeapi.Response = nil\n")
fmt.Fprint(sb, "\tresp2, err := cache.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp2 == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp2); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestSetcacheWithEncodeError(sb *strings.Builder) {
fmt.Fprintf(sb, "func TestCache%sSetcacheWithEncodeError(t *testing.T) {\n", d.APIStructName())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprintf(sb, "\tvar in []%s\n", d.CacheEntryName())
fmt.Fprint(sb, "\tff.fill(&in)\n")
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
fmt.Fprint(sb, "\t\tGobCodec: &FakeCodec{EncodeErr: errMocked},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\terr := cache.setcache(in)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestReadCacheNotFound(sb *strings.Builder) {
if fields := d.StructFields(d.Request); len(fields) <= 0 {
// this test cannot work when there are no fields in the
// request because we will always find a match.
// TODO(bassosimone): how to avoid having uncovered code?
return
}
fmt.Fprintf(sb, "func TestCache%sReadCacheNotFound(t *testing.T) {\n", d.APIStructName())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar incache []%s\n", d.CacheEntryName())
fmt.Fprint(sb, "\tff.fill(&incache)\n")
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\terr := cache.setcache(incache)\n")
fmt.Fprintf(sb, "\tif err != nil {\n")
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprintf(sb, "\tout, err := cache.readcache(req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errCacheNotFound) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif out != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil here\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWriteCacheDuplicate(sb *strings.Builder) {
fmt.Fprintf(sb, "func TestCache%sWriteCacheDuplicate(t *testing.T) {\n", d.APIStructName())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprintf(sb, "\tvar resp1 %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&resp1)\n")
fmt.Fprintf(sb, "\tvar resp2 %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&resp2)\n")
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\terr := cache.writecache(req, resp1)\n")
fmt.Fprintf(sb, "\tif err != nil {\n")
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\terr = cache.writecache(req, resp2)\n")
fmt.Fprintf(sb, "\tif err != nil {\n")
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tout, err := cache.readcache(req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif out == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil here\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(resp2, out); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestCachSizeLimited(sb *strings.Builder) {
if fields := d.StructFields(d.Request); len(fields) <= 0 {
// this test cannot work when there are no fields in the
// request because we will always find a match.
// TODO(bassosimone): how to avoid having uncovered code?
return
}
fmt.Fprintf(sb, "func TestCache%sCacheSizeLimited(t *testing.T) {\n", d.APIStructName())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tcache := &%s{\n", d.WithCacheAPIStructName())
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar prev int\n")
fmt.Fprintf(sb, "\tfor {\n")
fmt.Fprintf(sb, "\t\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\t\tff.fill(&req)\n")
fmt.Fprintf(sb, "\t\tvar resp %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\t\tff.fill(&resp)\n")
fmt.Fprintf(sb, "\t\terr := cache.writecache(req, resp)\n")
fmt.Fprintf(sb, "\t\tif err != nil {\n")
fmt.Fprintf(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprintf(sb, "\t\t}\n")
fmt.Fprintf(sb, "\t\tout, err := cache.getcache()\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif len(out) > prev {\n")
fmt.Fprint(sb, "\t\t\tprev = len(out)\n")
fmt.Fprint(sb, "\t\t\tcontinue\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tbreak\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
// GenCachingTestGo generates caching_test.go.
func GenCachingTestGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"errors\"\n")
fmt.Fprint(&sb, "\t\"testing\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
if desc.CachePolicy == CacheNone {
continue
}
desc.genTestCacheSuccess(&sb)
desc.genTestWriteCacheError(&sb)
desc.genTestFailureWithNoCache(&sb)
desc.genTestFailureWithPreviousCache(&sb)
desc.genTestSetcacheWithEncodeError(&sb)
desc.genTestReadCacheNotFound(&sb)
desc.genTestWriteCacheDuplicate(&sb)
desc.genTestCachSizeLimited(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,35 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) genNewCaller(sb *strings.Builder) {
fmt.Fprintf(sb, "// %s represents any type exposing a method\n",
d.CallerInterfaceName())
fmt.Fprintf(sb, "// like %s.Call.\n", d.APIStructName())
fmt.Fprintf(sb, "type %s interface {\n", d.CallerInterfaceName())
fmt.Fprintf(sb, "\tCall(ctx context.Context, req %s) (%s, error)\n",
d.RequestTypeName(), d.ResponseTypeName())
fmt.Fprint(sb, "}\n\n")
}
// GenCallersGo generates callers.go.
func GenCallersGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
desc.genNewCaller(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,104 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) clientMakeAPIBase(sb *strings.Builder) {
fmt.Fprintf(sb, "&%s{\n", d.APIStructName())
for _, field := range apiFields {
if field.ifLogin || field.ifTemplate {
continue
}
fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name)
}
fmt.Fprint(sb, "}")
}
func (d *Descriptor) clientMakeAPI(sb *strings.Builder) {
if d.RequiresLogin && d.CachePolicy != CacheNone {
panic("we don't support requiresLogin with caching")
}
if d.RequiresLogin {
fmt.Fprintf(sb, "&%s{\n", d.WithLoginAPIStructName())
fmt.Fprint(sb, "\tAPI:")
d.clientMakeAPIBase(sb)
fmt.Fprint(sb, ",\n")
fmt.Fprint(sb, "\tJSONCodec: c.JSONCodec,\n")
fmt.Fprint(sb, "\tKVStore: c.KVStore,\n")
fmt.Fprint(sb, "\tRegisterAPI: &simpleRegisterAPI{\n")
for _, field := range apiFields {
if field.ifLogin || field.ifTemplate {
continue
}
fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name)
}
fmt.Fprint(sb, "\t},\n")
fmt.Fprint(sb, "\tLoginAPI: &simpleLoginAPI{\n")
for _, field := range apiFields {
if field.ifLogin || field.ifTemplate {
continue
}
fmt.Fprintf(sb, "\t%s: c.%s,\n", field.name, field.name)
}
fmt.Fprint(sb, "\t},\n")
fmt.Fprint(sb, "}\n")
return
}
if d.CachePolicy != CacheNone {
fmt.Fprintf(sb, "&%s{\n", d.WithCacheAPIStructName())
fmt.Fprint(sb, "\tAPI:")
d.clientMakeAPIBase(sb)
fmt.Fprint(sb, ",\n")
fmt.Fprint(sb, "\tGobCodec: c.GobCodec,\n")
fmt.Fprint(sb, "\tKVStore: c.KVStore,\n")
fmt.Fprint(sb, "}\n")
return
}
d.clientMakeAPIBase(sb)
fmt.Fprint(sb, "\n")
}
func (d *Descriptor) genClientNewCaller(sb *strings.Builder) {
fmt.Fprintf(sb, "func (c *Client) new%sCaller() ", d.Name)
fmt.Fprintf(sb, "%s {\n", d.CallerInterfaceName())
fmt.Fprint(sb, "\treturn ")
d.clientMakeAPI(sb)
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genClientCall(sb *strings.Builder) {
fmt.Fprintf(sb, "// %s calls the %s API.\n", d.Name, d.Name)
fmt.Fprintf(sb, "func (c *Client) %s(\n", d.Name)
fmt.Fprintf(sb, "ctx context.Context, req %s,\n) ", d.RequestTypeName())
fmt.Fprintf(sb, "(%s, error) {\n", d.ResponseTypeName())
fmt.Fprintf(sb, "\tapi := c.new%sCaller()\n", d.Name)
fmt.Fprint(sb, "\treturn api.Call(ctx, req)\n")
fmt.Fprint(sb, "}\n\n")
}
// GenClientCallGo generates clientcall.go.
func GenClientCallGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
switch desc.Name {
case "Register", "Login":
// We don't want to generate these APIs as toplevel.
continue
}
desc.genClientNewCaller(&sb)
desc.genClientCall(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,182 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) genTestClientCallRoundTrip(sb *strings.Builder) {
// generate the type of the handler
fmt.Fprintf(sb, "type handleClientCall%s struct {\n", d.Name)
fmt.Fprint(sb, "\taccept string\n")
fmt.Fprint(sb, "\tbody []byte\n")
fmt.Fprint(sb, "\tcontentType string\n")
fmt.Fprint(sb, "\tcount int32\n")
fmt.Fprint(sb, "\tmethod string\n")
fmt.Fprint(sb, "\tmu sync.Mutex\n")
fmt.Fprintf(sb, "\tresp %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\turl *url.URL\n")
fmt.Fprint(sb, "\tuserAgent string\n")
fmt.Fprint(sb, "}\n\n")
// generate the handling function
fmt.Fprintf(sb,
"func (h *handleClientCall%s) ServeHTTP(w http.ResponseWriter, r *http.Request) {",
d.Name)
fmt.Fprint(sb, "\tff := fakeFill{}\n")
if d.RequiresLogin {
fmt.Fprintf(sb, "\tif r.URL.Path == \"/api/v1/register\" {\n")
fmt.Fprintf(sb, "\t\tvar out apimodel.RegisterResponse\n")
fmt.Fprintf(sb, "\t\tff.fill(&out)\n")
fmt.Fprintf(sb, "\t\tdata, err := json.Marshal(out)\n")
fmt.Fprintf(sb, "\t\tif err != nil {\n")
fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n")
fmt.Fprintf(sb, "\t\t\treturn\n")
fmt.Fprintf(sb, "\t\t}\n")
fmt.Fprintf(sb, "\t\tw.Write(data)\n")
fmt.Fprintf(sb, "\t\treturn\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tif r.URL.Path == \"/api/v1/login\" {\n")
fmt.Fprintf(sb, "\t\tvar out apimodel.LoginResponse\n")
fmt.Fprintf(sb, "\t\tff.fill(&out)\n")
fmt.Fprintf(sb, "\t\tdata, err := json.Marshal(out)\n")
fmt.Fprintf(sb, "\t\tif err != nil {\n")
fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n")
fmt.Fprintf(sb, "\t\t\treturn\n")
fmt.Fprintf(sb, "\t\t}\n")
fmt.Fprintf(sb, "\t\tw.Write(data)\n")
fmt.Fprintf(sb, "\t\treturn\n")
fmt.Fprintf(sb, "\t}\n")
}
fmt.Fprint(sb, "\tdefer h.mu.Unlock()\n")
fmt.Fprint(sb, "\th.mu.Lock()\n")
fmt.Fprint(sb, "\tif h.count > 0 {\n")
fmt.Fprint(sb, "\t\tw.WriteHeader(400)\n")
fmt.Fprint(sb, "\t\treturn\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\th.count++\n")
fmt.Fprint(sb, "\tif r.Body != nil {\n")
fmt.Fprint(sb, "\t\tdata, err := ioutil.ReadAll(r.Body)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprintf(sb, "\t\t\tw.WriteHeader(400)\n")
fmt.Fprintf(sb, "\t\t\treturn\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\th.body = data\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\th.method = r.Method\n")
fmt.Fprint(sb, "\th.url = r.URL\n")
fmt.Fprint(sb, "\th.accept = r.Header.Get(\"Accept\")\n")
fmt.Fprint(sb, "\th.contentType = r.Header.Get(\"Content-Type\")\n")
fmt.Fprint(sb, "\th.userAgent = r.Header.Get(\"User-Agent\")\n")
fmt.Fprintf(sb, "\tvar out %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&out)\n")
fmt.Fprintf(sb, "\th.resp = out\n")
fmt.Fprintf(sb, "\tdata, err := json.Marshal(out)\n")
fmt.Fprintf(sb, "\tif err != nil {\n")
fmt.Fprintf(sb, "\t\tw.WriteHeader(400)\n")
fmt.Fprintf(sb, "\t\treturn\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tw.Write(data)\n")
fmt.Fprintf(sb, "\t}\n\n")
// generate the test itself
fmt.Fprintf(sb, "func Test%sClientCallRoundTrip(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\t// setup\n")
fmt.Fprintf(sb, "\thandler := &handleClientCall%s{}\n", d.Name)
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
fmt.Fprintf(sb, "\treq := &%s{}\n", d.RequestTypeNameAsStruct())
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tclnt := &Client{KVStore: &kvstore.Memory{}, BaseURL: srvr.URL}\n")
fmt.Fprint(sb, "\tff.fill(&clnt.UserAgent)\n")
fmt.Fprint(sb, "\t// issue request\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprintf(sb, "\tresp, err := clnt.%s(ctx, req)\n", d.Name)
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response here\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// compare our response and server's one\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.resp, resp); diff != \"\" {")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// check whether headers are OK\n")
fmt.Fprint(sb, "\tif handler.accept != \"application/json\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid accept header\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.userAgent != clnt.UserAgent {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid user-agent header\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// check whether the method is OK\n")
fmt.Fprintf(sb, "\tif handler.method != \"%s\" {\n", d.Method)
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid method\")\n")
fmt.Fprint(sb, "\t}\n")
if d.Method == "POST" {
fmt.Fprint(sb, "\t// check the body\n")
fmt.Fprint(sb, "\tif handler.contentType != \"application/json\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid content-type header\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tgot := &%s{}\n", d.RequestTypeNameAsStruct())
fmt.Fprintf(sb, "\tif err := json.Unmarshal(handler.body, &got); err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(req, got); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
} else {
fmt.Fprint(sb, "\t// check the query\n")
fmt.Fprintf(sb, "\tapi := &%s{BaseURL: srvr.URL}\n", d.APIStructName())
fmt.Fprint(sb, "\thttpReq, err := api.newRequest(context.Background(), req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.Path, httpReq.URL.Path); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(handler.url.RawQuery, httpReq.URL.RawQuery); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
}
fmt.Fprint(sb, "}\n\n")
}
// GenClientCallTestGo generates clientcall_test.go.
func GenClientCallTestGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"encoding/json\"\n")
fmt.Fprint(&sb, "\t\"io/ioutil\"\n")
fmt.Fprint(&sb, "\t\"net/http/httptest\"\n")
fmt.Fprint(&sb, "\t\"net/http\"\n")
fmt.Fprint(&sb, "\t\"net/url\"\n")
fmt.Fprint(&sb, "\t\"testing\"\n")
fmt.Fprint(&sb, "\t\"sync\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
if desc.Name == "Login" || desc.Name == "Register" {
continue // they cannot be called directly
}
desc.genTestClientCallRoundTrip(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,32 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) genNewCloner(sb *strings.Builder) {
fmt.Fprintf(sb, "// %s represents any type exposing a method\n",
d.ClonerInterfaceName())
fmt.Fprintf(sb, "// like %s.WithToken.\n", d.APIStructName())
fmt.Fprintf(sb, "type %s interface {\n", d.ClonerInterfaceName())
fmt.Fprintf(sb, "\tWithToken(token string) %s\n", d.CallerInterfaceName())
fmt.Fprint(sb, "}\n\n")
}
// GenClonersGo generates cloners.go.
func GenClonersGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
for _, desc := range Descriptors {
if !desc.RequiresLogin {
continue
}
desc.genNewCloner(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,61 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) genNewFakeAPI(sb *strings.Builder) {
fmt.Fprintf(sb, "type %s struct {\n", d.FakeAPIStructName())
if d.RequiresLogin {
fmt.Fprintf(sb, "\tWithResult %s\n", d.CallerInterfaceName())
}
fmt.Fprint(sb, "\tErr error\n")
fmt.Fprintf(sb, "\tResponse %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tCountCall *atomicx.Int64\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (fapi *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
d.FakeAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
fmt.Fprint(sb, "\tif fapi.CountCall != nil {\n")
fmt.Fprint(sb, "\t\tfapi.CountCall.Add(1)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn fapi.Response, fapi.Err\n")
fmt.Fprint(sb, "}\n\n")
if d.RequiresLogin {
fmt.Fprintf(sb, "func (fapi *%s) WithToken(token string) %s {\n",
d.FakeAPIStructName(), d.CallerInterfaceName())
fmt.Fprint(sb, "\treturn fapi.WithResult\n")
fmt.Fprint(sb, "}\n\n")
}
fmt.Fprint(sb, "var (\n")
fmt.Fprintf(sb, "\t_ %s = &%s{}\n", d.CallerInterfaceName(),
d.FakeAPIStructName())
if d.RequiresLogin {
fmt.Fprintf(sb, "\t_ %s = &%s{}\n", d.ClonerInterfaceName(),
d.FakeAPIStructName())
}
fmt.Fprint(sb, ")\n\n")
}
// GenFakeAPITestGo generates fakeapi_test.go.
func GenFakeAPITestGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/atomicx\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
desc.genNewFakeAPI(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,57 @@
// Command generator generates code in the ooapi package.
//
// To this end, it uses the content of the apimodel package as
// well as the content of the spec.go file.
//
// The apimodel package defines the model, i.e., the structure
// of requests and responses and how messages should be sent
// and received.
//
// The spec.go file describes all the implemented APIs.
//
// If you change apimodel or spec.go, remember to run the
// `go generate ./...` command to regenerate all files.
package main
import (
"flag"
"fmt"
)
var flagFile = flag.String("file", "", "Indicate which file to regenerate")
func main() {
flag.Parse()
switch file := *flagFile; file {
case "apis.go":
GenAPIsGo(file)
case "responses.go":
GenResponsesGo(file)
case "requests.go":
GenRequestsGo(file)
case "swagger_test.go":
GenSwaggerTestGo(file)
case "apis_test.go":
GenAPIsTestGo(file)
case "callers.go":
GenCallersGo(file)
case "caching.go":
GenCachingGo(file)
case "login.go":
GenLoginGo(file)
case "cloners.go":
GenClonersGo(file)
case "fakeapi_test.go":
GenFakeAPITestGo(file)
case "caching_test.go":
GenCachingTestGo(file)
case "login_test.go":
GenLoginTestGo(file)
case "clientcall.go":
GenClientCallGo(file)
case "clientcall_test.go":
GenClientCallTestGo(file)
default:
panic(fmt.Sprintf("don't know how to create this file: %s", file))
}
}

View file

@ -0,0 +1,182 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) genNewLogin(sb *strings.Builder) {
fmt.Fprintf(sb, "// %s implements login for %s.\n",
d.WithLoginAPIStructName(), d.APIStructName())
fmt.Fprintf(sb, "type %s struct {\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\tAPI %s // mandatory\n", d.ClonerInterfaceName())
fmt.Fprint(sb, "\tJSONCodec JSONCodec // optional\n")
fmt.Fprint(sb, "\tKVStore KVStore // mandatory\n")
fmt.Fprint(sb, "\tRegisterAPI callerForRegisterAPI // mandatory\n")
fmt.Fprint(sb, "\tLoginAPI callerForLoginAPI // mandatory\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "// Call logins, if needed, then calls the API.\n")
fmt.Fprintf(sb, "func (api *%s) Call(ctx context.Context, req %s) (%s, error) {\n",
d.WithLoginAPIStructName(), d.RequestTypeName(), d.ResponseTypeName())
fmt.Fprint(sb, "\ttoken, err := api.maybeLogin(ctx)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tresp, err := api.API.WithToken(token).Call(ctx, req)\n")
fmt.Fprint(sb, "\tif errors.Is(err, ErrUnauthorized) {\n")
fmt.Fprint(sb, "\t\t// Maybe the clock is just off? Let's try to obtain\n")
fmt.Fprint(sb, "\t\t// a token again and see if this fixes it.\n")
fmt.Fprint(sb, "\t\tif token, err = api.forceLogin(ctx); err == nil {\n")
fmt.Fprint(sb, "\t\t\tswitch resp, err = api.API.WithToken(token).Call(ctx, req); err {\n")
fmt.Fprint(sb, "\t\t\tcase nil:\n")
fmt.Fprint(sb, "\t\t\t\treturn resp, nil\n")
fmt.Fprint(sb, "\t\t\tcase ErrUnauthorized:\n")
fmt.Fprint(sb, "\t\t\t\t// fallthrough\n")
fmt.Fprint(sb, "\t\t\tdefault:\n")
fmt.Fprint(sb, "\t\t\t\treturn nil, err\n")
fmt.Fprint(sb, "\t\t\t}\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\t// Okay, this seems a broader problem. How about we try\n")
fmt.Fprint(sb, "\t\t// and re-register ourselves again instead?\n")
fmt.Fprint(sb, "\t\ttoken, err = api.forceRegister(ctx)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\treturn nil, err\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tresp, err = api.API.WithToken(token).Call(ctx, req)\n")
fmt.Fprint(sb, "\t\t// fallthrough\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn resp, nil\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) jsonCodec() JSONCodec {\n",
d.WithLoginAPIStructName())
fmt.Fprint(sb, "\tif api.JSONCodec != nil {\n")
fmt.Fprint(sb, "\t\treturn api.JSONCodec\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &defaultJSONCodec{}\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) readstate() (*loginState, error) {\n",
d.WithLoginAPIStructName())
fmt.Fprint(sb, "\tdata, err := api.KVStore.Get(loginKey)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tvar ls loginState\n")
fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, &ls); err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn &ls, nil\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) writestate(ls *loginState) error {\n",
d.WithLoginAPIStructName())
fmt.Fprint(sb, "\tdata, err := api.jsonCodec().Encode(*ls)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn api.KVStore.Set(loginKey, data)\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) doRegister(ctx context.Context, password string) (string, error) {\n",
d.WithLoginAPIStructName())
fmt.Fprint(sb, "\treq := newRegisterRequest(password)\n")
fmt.Fprint(sb, "\tls := &loginState{}\n")
fmt.Fprint(sb, "\tresp, err := api.RegisterAPI.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn \"\", err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tls.ClientID = resp.ClientID\n")
fmt.Fprint(sb, "\tls.Password = req.Password\n")
fmt.Fprint(sb, "\treturn api.doLogin(ctx, ls)\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) forceRegister(ctx context.Context) (string, error) {\n",
d.WithLoginAPIStructName())
fmt.Fprint(sb, "\tvar password string\n")
fmt.Fprint(sb, "\t// If we already have a previous password, let us keep\n")
fmt.Fprint(sb, "\t// using it. This will allow a new version of the API to\n")
fmt.Fprint(sb, "\t// be able to continue to identify this probe. (This\n")
fmt.Fprint(sb, "\t// assumes that we have a stateless API that generates\n")
fmt.Fprint(sb, "\t// the user ID as a signature of the password plus a\n")
fmt.Fprint(sb, "\t// timestamp and that the key to generate the signature\n")
fmt.Fprint(sb, "\t// is not lost. If all these conditions are met, we\n")
fmt.Fprint(sb, "\t// can then serve better test targets to more long running\n")
fmt.Fprint(sb, "\t// (and therefore trusted) probes.)\n")
fmt.Fprint(sb, "\tif ls, err := api.readstate(); err == nil {\n")
fmt.Fprint(sb, "\t\tpassword = ls.Password\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif password == \"\" {\n")
fmt.Fprint(sb, "\t\tpassword = newRandomPassword()\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn api.doRegister(ctx, password)\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) forceLogin(ctx context.Context) (string, error) {\n",
d.WithLoginAPIStructName())
fmt.Fprint(sb, "\tls, err := api.readstate()\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn \"\", err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn api.doLogin(ctx, ls)\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) maybeLogin(ctx context.Context) (string, error) {\n",
d.WithLoginAPIStructName())
fmt.Fprint(sb, "\tls, _ := api.readstate()\n")
fmt.Fprint(sb, "\tif ls == nil || !ls.credentialsValid() {\n")
fmt.Fprint(sb, "\t\treturn api.forceRegister(ctx)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif !ls.tokenValid() {\n")
fmt.Fprint(sb, "\t\treturn api.doLogin(ctx, ls)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn ls.Token, nil\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "func (api *%s) doLogin(ctx context.Context, ls *loginState) (string, error) {\n",
d.WithLoginAPIStructName())
fmt.Fprint(sb, "\treq := &apimodel.LoginRequest{\n")
fmt.Fprint(sb, "\t\tClientID: ls.ClientID,\n")
fmt.Fprint(sb, "\t\tPassword: ls.Password,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tresp, err := api.LoginAPI.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn \"\", err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tls.Token = resp.Token\n")
fmt.Fprint(sb, "\tls.Expire = resp.Expire\n")
fmt.Fprint(sb, "\tif err := api.writestate(ls); err != nil {\n")
fmt.Fprint(sb, "\t\treturn \"\", err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\treturn ls.Token, nil\n")
fmt.Fprint(sb, "}\n\n")
fmt.Fprintf(sb, "var _ %s = &%s{}\n\n", d.CallerInterfaceName(),
d.WithLoginAPIStructName())
}
// GenLoginGo generates login.go.
func GenLoginGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"errors\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
if !desc.RequiresLogin {
continue
}
desc.genNewLogin(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,899 @@
package main
import (
"fmt"
"strings"
"time"
)
func (d *Descriptor) genTestRegisterAndLoginSuccess(sb *strings.Builder) {
fmt.Fprintf(sb, "func TestRegisterAndLogin%sSuccess(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestContinueUsingToken(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sContinueUsingToken(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
fmt.Fprint(sb, "\t{\n")
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif resp == nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif loginAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif registerAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// step 2: we disable register and login but we\n")
fmt.Fprint(sb, "\t// should be okay because of the token\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tregisterAPI.Err = errMocked\n")
fmt.Fprint(sb, "\tregisterAPI.Response = nil\n")
fmt.Fprint(sb, "\tloginAPI.Err = errMocked\n")
fmt.Fprint(sb, "\tloginAPI.Response = nil\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWithValidButExpiredToken(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWithValidButExpiredToken(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
fmt.Fprint(sb, "\t\tErr: errMocked,\n")
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tls := &loginState{\n")
fmt.Fprintf(sb, "\t\tClientID: \"antani-antani\",\n")
fmt.Fprintf(sb, "\t\tExpire: time.Now().Add(-5 * time.Second),\n")
fmt.Fprintf(sb, "\t\tToken: \"antani-antani-token\",\n")
fmt.Fprintf(sb, "\t\tPassword: \"antani-antani-password\",\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tif err := login.writestate(ls); err != nil {\n")
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif diff := cmp.Diff(expect, resp); diff != \"\" {\n")
fmt.Fprint(sb, "\t\tt.Fatal(diff)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 0 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWithRegisterAPIError(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWithRegisterAPIError(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
fmt.Fprint(sb, "\t\tErr: errMocked,\n")
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestWithLoginFailure(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sWithLoginFailure(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
fmt.Fprint(sb, "\t\t\tErr: errMocked,\n")
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestRegisterAndLoginThenFail(sb *strings.Builder) {
fmt.Fprintf(sb, "func TestRegisterAndLogin%sThenFail(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\t\tErr: errMocked,\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestTheDatabaseIsReplaced(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sTheDatabaseIsReplaced(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tt: t,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
fmt.Fprint(sb, "\t{\n")
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif resp == nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// step 2: we forget accounts and try again.\n")
fmt.Fprint(sb, "\thandler.forgetLogins()\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.logins.Load() != 3 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestTheDatabaseIsReplacedThenFailure(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sTheDatabaseIsReplacedThenFailure(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tt: t,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
fmt.Fprint(sb, "\t{\n")
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif resp == nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// step 2: we forget accounts and try again.\n")
fmt.Fprint(sb, "\t// but registrations are also failing.\n")
fmt.Fprint(sb, "\thandler.forgetLogins()\n")
fmt.Fprint(sb, "\thandler.noRegister = true\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestRegisterAndLoginCannotWriteState(sb *strings.Builder) {
fmt.Fprintf(sb, "func TestRegisterAndLogin%sCannotWriteState(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprint(sb, "\tregisterAPI := &FakeRegisterAPI{\n")
fmt.Fprint(sb, "\t\tResponse: &apimodel.RegisterResponse{\n")
fmt.Fprint(sb, "\t\t\tClientID: \"antani-antani\",\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &FakeLoginAPI{\n")
fmt.Fprint(sb, "\t\t\tResponse: &apimodel.LoginResponse{\n")
fmt.Fprint(sb, "\t\t\t\tExpire: time.Now().Add(3600*time.Second),\n")
fmt.Fprint(sb, "\t\t\t\tToken: \"antani-antani-token\",\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t\tCountCall: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\t\tAPI: &%s{\n", d.FakeAPIStructName())
fmt.Fprintf(sb, "\t\t\tWithResult: &%s{\n", d.FakeAPIStructName())
fmt.Fprint(sb, "\t\t\t\tResponse: expect,\n")
fmt.Fprint(sb, "\t\t\t},\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\t\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{\n")
fmt.Fprint(sb, "\t\t\tEncodeErr: errMocked,\n")
fmt.Fprint(sb, "\t\t},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, errMocked) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif loginAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid loginAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif registerAPI.CountCall.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid registerAPI.CountCall\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestReadStateDecodeFailure(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sReadStateDecodeFailure(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprintf(sb, "\tvar expect %s\n", d.ResponseTypeName())
fmt.Fprint(sb, "\tff.fill(&expect)\n")
fmt.Fprint(sb, "\terrMocked := errors.New(\"mocked error\")\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprint(sb, "\t\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t\tJSONCodec: &FakeCodec{DecodeErr: errMocked},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tls := &loginState{\n")
fmt.Fprintf(sb, "\t\tClientID: \"antani-antani\",\n")
fmt.Fprintf(sb, "\t\tExpire: time.Now().Add(-5 * time.Second),\n")
fmt.Fprintf(sb, "\t\tToken: \"antani-antani-token\",\n")
fmt.Fprintf(sb, "\t\tPassword: \"antani-antani-password\",\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tif err := login.writestate(ls); err != nil {\n")
fmt.Fprintf(sb, "\t\tt.Fatal(err)\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "\tout, err := login.forceLogin(context.Background())\n")
fmt.Fprintf(sb, "if !errors.Is(err, errMocked) {\n")
fmt.Fprintf(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprintf(sb, "if out != \"\" {\n")
fmt.Fprintf(sb, "\t\tt.Fatal(\"expected empty string here\")\n")
fmt.Fprintf(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestClockIsOffThenSuccess(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sClockIsOffThenSuccess(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tt: t,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
fmt.Fprint(sb, "\t{\n")
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif resp == nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n")
fmt.Fprint(sb, "\t// this should simulate the client clock\n")
fmt.Fprint(sb, "\t// being off and considering a token still valid\n")
fmt.Fprint(sb, "\thandler.forgetTokens()\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.registers.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestClockIsOffThen401(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sClockIsOffThen401(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tt: t,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
fmt.Fprint(sb, "\t{\n")
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif resp == nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n")
fmt.Fprint(sb, "\t// this should simulate the client clock\n")
fmt.Fprint(sb, "\t// being off and considering a token still valid\n")
fmt.Fprint(sb, "\thandler.forgetTokens()\n")
fmt.Fprint(sb, "\thandler.failCallWith = []int{401, 401}\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp == nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.logins.Load() != 3 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.registers.Load() != 2 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
func (d *Descriptor) genTestClockIsOffThen500(sb *strings.Builder) {
fmt.Fprintf(sb, "func Test%sClockIsOffThen500(t *testing.T) {\n", d.Name)
fmt.Fprint(sb, "\tff := &fakeFill{}\n")
fmt.Fprint(sb, "\thandler := &LoginHandler{\n")
fmt.Fprint(sb, "\t\tlogins: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tregisters: &atomicx.Int64{},\n")
fmt.Fprint(sb, "\t\tt: t,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tsrvr := httptest.NewServer(handler)\n")
fmt.Fprint(sb, "\tdefer srvr.Close()\n")
fmt.Fprint(sb, "\tregisterAPI := &simpleRegisterAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t\tloginAPI := &simpleLoginAPI{\n")
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprintf(sb, "\tbaseAPI := &%s{\n", d.APIStructName())
fmt.Fprint(sb, "\t\tHTTPClient: &VerboseHTTPClient{T: t},\n")
fmt.Fprint(sb, "\t\tBaseURL: srvr.URL,\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tlogin := &%s{\n", d.WithLoginAPIStructName())
fmt.Fprintf(sb, "\tAPI : baseAPI,\n")
fmt.Fprint(sb, "\tRegisterAPI: registerAPI,\n")
fmt.Fprint(sb, "\tLoginAPI: loginAPI,\n")
fmt.Fprint(sb, "\tKVStore: &kvstore.Memory{},\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tvar req %s\n", d.RequestTypeName())
fmt.Fprint(sb, "\tff.fill(&req)\n")
fmt.Fprint(sb, "\tctx := context.Background()\n")
fmt.Fprint(sb, "\t// step 1: we register and login and use the token\n")
fmt.Fprint(sb, "\t// inside a scope just to avoid mistakes\n")
fmt.Fprint(sb, "\t{\n")
fmt.Fprint(sb, "\t\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\t\tif err != nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(err)\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif resp == nil {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"expected non-nil response\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.logins.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t\tif handler.registers.Load() != 1 {\n")
fmt.Fprint(sb, "\t\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t\t}\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\t// step 2: we forget tokens and try again.\n")
fmt.Fprint(sb, "\t// this should simulate the client clock\n")
fmt.Fprint(sb, "\t// being off and considering a token still valid\n")
fmt.Fprint(sb, "\thandler.forgetTokens()\n")
fmt.Fprint(sb, "\thandler.failCallWith = []int{401, 500}\n")
fmt.Fprint(sb, "\tresp, err := login.Call(ctx, req)\n")
fmt.Fprint(sb, "\tif !errors.Is(err, ErrHTTPFailure) {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"not the error we expected\", err)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp != nil {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"expected nil response\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.logins.Load() != 2 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.logins\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif handler.registers.Load() != 1 {\n")
fmt.Fprint(sb, "\t\tt.Fatal(\"invalid handler.registers\")\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "}\n\n")
}
// GenLoginTestGo generates login_test.go.
func GenLoginTestGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"errors\"\n")
fmt.Fprint(&sb, "\t\"net/http/httptest\"\n")
fmt.Fprint(&sb, "\t\"testing\"\n")
fmt.Fprint(&sb, "\t\"time\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/google/go-cmp/cmp\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/atomicx\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/kvstore\"\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n")
for _, desc := range Descriptors {
if !desc.RequiresLogin {
continue
}
desc.genTestRegisterAndLoginSuccess(&sb)
desc.genTestContinueUsingToken(&sb)
desc.genTestWithValidButExpiredToken(&sb)
desc.genTestWithRegisterAPIError(&sb)
desc.genTestWithLoginFailure(&sb)
desc.genTestRegisterAndLoginThenFail(&sb)
desc.genTestTheDatabaseIsReplaced(&sb)
desc.genTestRegisterAndLoginCannotWriteState(&sb)
desc.genTestReadStateDecodeFailure(&sb)
desc.genTestTheDatabaseIsReplacedThenFailure(&sb)
desc.genTestClockIsOffThenSuccess(&sb)
desc.genTestClockIsOffThen401(&sb)
desc.genTestClockIsOffThen500(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,153 @@
package main
import (
"fmt"
"reflect"
)
// TypeName returns v's package-qualified type name.
func (d *Descriptor) TypeName(v interface{}) string {
return reflect.TypeOf(v).String()
}
// RequestTypeName calls d.TypeName(d.Request).
func (d *Descriptor) RequestTypeName() string {
return d.TypeName(d.Request)
}
// ResponseTypeName calls d.TypeName(d.Response).
func (d *Descriptor) ResponseTypeName() string {
return d.TypeName(d.Response)
}
// APIStructName returns the correct struct type name
// for the API we're currently processing.
func (d *Descriptor) APIStructName() string {
return fmt.Sprintf("simple%sAPI", d.Name)
}
// FakeAPIStructName returns the correct struct type name
// for the fake for the API we're currently processing.
func (d *Descriptor) FakeAPIStructName() string {
return fmt.Sprintf("Fake%sAPI", d.Name)
}
// WithLoginAPIStructName returns the correct struct type name
// for the WithLoginAPI we're currently processing.
func (d *Descriptor) WithLoginAPIStructName() string {
return fmt.Sprintf("withLogin%sAPI", d.Name)
}
// CallerInterfaceName returns the correct caller interface name
// for the API we're currently processing.
func (d *Descriptor) CallerInterfaceName() string {
return fmt.Sprintf("callerFor%sAPI", d.Name)
}
// ClonerInterfaceName returns the correct cloner interface name
// for the API we're currently processing.
func (d *Descriptor) ClonerInterfaceName() string {
return fmt.Sprintf("clonerFor%sAPI", d.Name)
}
// WithCacheAPIStructName returns the correct struct type name for
// the cache for the API we're currently processing.
func (d *Descriptor) WithCacheAPIStructName() string {
return fmt.Sprintf("withCache%sAPI", d.Name)
}
// CacheEntryName returns the correct struct type name for the
// cache entry for the API we're currently processing.
func (d *Descriptor) CacheEntryName() string {
return fmt.Sprintf("cacheEntryFor%sAPI", d.Name)
}
// CacheKey returns the correct cache key for the API
// we're currently processing.
func (d *Descriptor) CacheKey() string {
return fmt.Sprintf("%s.cache", d.Name)
}
// StructFields returns all the struct fields of in. This function
// assumes that in is a pointer to struct, and will otherwise panic.
func (d *Descriptor) StructFields(in interface{}) []*reflect.StructField {
t := reflect.TypeOf(in)
if t.Kind() != reflect.Ptr {
panic("not a pointer")
}
t = t.Elem()
if t.Kind() != reflect.Struct {
panic("not a struct")
}
var out []*reflect.StructField
for idx := 0; idx < t.NumField(); idx++ {
f := t.Field(idx)
out = append(out, &f)
}
return out
}
// StructFieldsWithTag returns all the struct fields of
// in that have the specified tag.
func (d *Descriptor) StructFieldsWithTag(in interface{}, tag string) []*reflect.StructField {
var out []*reflect.StructField
for _, f := range d.StructFields(in) {
if f.Tag.Get(tag) != "" {
out = append(out, f)
}
}
return out
}
// RequestOrResponseTypeKind returns the type kind of in, which should
// be a request or a response. This function assumes that in is either a
// pointer to struct or a map and will panic otherwise.
func (d *Descriptor) RequestOrResponseTypeKind(in interface{}) reflect.Kind {
t := reflect.TypeOf(in)
if t.Kind() == reflect.Ptr {
t = t.Elem()
if t.Kind() != reflect.Struct {
panic("not a struct")
}
return reflect.Struct
}
if t.Kind() != reflect.Map {
panic("not a map")
}
return reflect.Map
}
// RequestTypeKind calls d.RequestOrResponseTypeKind(d.Request).
func (d *Descriptor) RequestTypeKind() reflect.Kind {
return d.RequestOrResponseTypeKind(d.Request)
}
// ResponseTypeKind calls d.RequestOrResponseTypeKind(d.Response).
func (d *Descriptor) ResponseTypeKind() reflect.Kind {
return d.RequestOrResponseTypeKind(d.Response)
}
// TypeNameAsStruct assumes that in is a pointer to struct and
// returns the type of the corresponding struct. The returned
// type is package qualified.
func (d *Descriptor) TypeNameAsStruct(in interface{}) string {
t := reflect.TypeOf(in)
if t.Kind() != reflect.Ptr {
panic("not a pointer")
}
t = t.Elem()
if t.Kind() != reflect.Struct {
panic("not a struct")
}
return t.String()
}
// RequestTypeNameAsStruct calls d.TypeNameAsStruct(d.Request)
func (d *Descriptor) RequestTypeNameAsStruct() string {
return d.TypeNameAsStruct(d.Request)
}
// ResponseTypeNameAsStruct calls d.TypeNameAsStruct(d.Response)
func (d *Descriptor) ResponseTypeNameAsStruct() string {
return d.TypeNameAsStruct(d.Response)
}

View file

@ -0,0 +1,141 @@
package main
import (
"fmt"
"reflect"
"strings"
"time"
)
const (
tagForQuery = "query"
tagForRequired = "required"
)
func (d *Descriptor) genNewRequestQueryElemString(sb *strings.Builder, f *reflect.StructField) {
name := f.Name
query := f.Tag.Get(tagForQuery)
if f.Tag.Get(tagForRequired) == "true" {
fmt.Fprintf(sb, "\tif req.%s == \"\" {\n", name)
fmt.Fprintf(sb, "\t\treturn nil, newErrEmptyField(\"%s\")\n", name)
fmt.Fprint(sb, "\t}\n")
fmt.Fprintf(sb, "\tq.Add(\"%s\", req.%s)\n", query, name)
return
}
fmt.Fprintf(sb, "\tif req.%s != \"\" {\n", name)
fmt.Fprintf(sb, "\t\tq.Add(\"%s\", req.%s)\n", query, name)
fmt.Fprint(sb, "\t}\n")
}
func (d *Descriptor) genNewRequestQueryElemBool(sb *strings.Builder, f *reflect.StructField) {
// required does not make much sense for a boolean field
name := f.Name
query := f.Tag.Get(tagForQuery)
fmt.Fprintf(sb, "\tif req.%s {\n", name)
fmt.Fprintf(sb, "\t\tq.Add(\"%s\", \"true\")\n", query)
fmt.Fprint(sb, "\t}\n")
}
func (d *Descriptor) genNewRequestQueryElemInt64(sb *strings.Builder, f *reflect.StructField) {
// required does not make much sense for an integer field
name := f.Name
query := f.Tag.Get(tagForQuery)
fmt.Fprintf(sb, "\tif req.%s != 0 {\n", name)
fmt.Fprintf(sb, "\t\tq.Add(\"%s\", newQueryFieldInt64(req.%s))\n", query, name)
fmt.Fprint(sb, "\t}\n")
}
func (d *Descriptor) genNewRequestQuery(sb *strings.Builder) {
if d.Method != "GET" {
return // we only generate query for GET
}
fields := d.StructFieldsWithTag(d.Request, tagForQuery)
if len(fields) <= 0 {
return
}
fmt.Fprint(sb, "\tq := url.Values{}\n")
for idx, f := range fields {
switch f.Type.Kind() {
case reflect.String:
d.genNewRequestQueryElemString(sb, f)
case reflect.Bool:
d.genNewRequestQueryElemBool(sb, f)
case reflect.Int64:
d.genNewRequestQueryElemInt64(sb, f)
default:
panic(fmt.Sprintf("unexpected query type at index %d", idx))
}
}
fmt.Fprint(sb, "\tURL.RawQuery = q.Encode()\n")
}
func (d *Descriptor) genNewRequestCallNewRequest(sb *strings.Builder) {
if d.Method == "POST" {
fmt.Fprint(sb, "\tbody, err := api.jsonCodec().Encode(req)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tout, err := api.requestMaker().NewRequest(")
fmt.Fprintf(sb, "ctx, \"%s\", URL.String(), ", d.Method)
fmt.Fprint(sb, "bytes.NewReader(body))\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tout.Header.Set(\"Content-Type\", \"application/json\")\n")
fmt.Fprint(sb, "\treturn out, nil\n")
return
}
fmt.Fprint(sb, "\treturn api.requestMaker().NewRequest(")
fmt.Fprintf(sb, "ctx, \"%s\", URL.String(), ", d.Method)
fmt.Fprint(sb, "nil)\n")
}
func (d *Descriptor) genNewRequest(sb *strings.Builder) {
fmt.Fprintf(
sb, "func (api *%s) newRequest(ctx context.Context, req %s) %s {\n",
d.APIStructName(), d.RequestTypeName(), "(*http.Request, error)")
fmt.Fprint(sb, "\tURL, err := url.Parse(api.baseURL())\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
switch d.URLPath.IsTemplate {
case false:
fmt.Fprintf(sb, "\tURL.Path = \"%s\"\n", d.URLPath.Value)
case true:
fmt.Fprintf(
sb, "\tup, err := api.templateExecutor().Execute(\"%s\", req)\n",
d.URLPath.Value)
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tURL.Path = up\n")
}
d.genNewRequestQuery(sb)
d.genNewRequestCallNewRequest(sb)
fmt.Fprintf(sb, "}\n\n")
}
// GenRequestsGo generates requests.go.
func GenRequestsGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"bytes\"\n")
fmt.Fprint(&sb, "\t\"context\"\n")
fmt.Fprint(&sb, "\t\"net/http\"\n")
fmt.Fprint(&sb, "\t\"net/url\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n\n")
for _, desc := range Descriptors {
desc.genNewRequest(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,80 @@
package main
import (
"fmt"
"reflect"
"strings"
"time"
)
func (d *Descriptor) genNewResponse(sb *strings.Builder) {
fmt.Fprintf(sb,
"func (api *%s) newResponse(resp *http.Response, err error) (%s, error) {\n",
d.APIStructName(), d.ResponseTypeName())
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp.StatusCode == 401 {\n")
fmt.Fprint(sb, "\t\treturn nil, ErrUnauthorized\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tif resp.StatusCode != 200 {\n")
fmt.Fprint(sb, "\t\treturn nil, newHTTPFailure(resp.StatusCode)\n")
fmt.Fprint(sb, "\t}\n")
fmt.Fprint(sb, "\tdefer resp.Body.Close()\n")
fmt.Fprint(sb, "\treader := io.LimitReader(resp.Body, 4<<20)\n")
fmt.Fprint(sb, "\tdata, err := ioutil.ReadAll(reader)\n")
fmt.Fprint(sb, "\tif err != nil {\n")
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
switch d.ResponseTypeKind() {
case reflect.Map:
fmt.Fprintf(sb, "\tout := %s{}\n", d.ResponseTypeName())
case reflect.Struct:
fmt.Fprintf(sb, "\tout := &%s{}\n", d.ResponseTypeNameAsStruct())
}
switch d.ResponseTypeKind() {
case reflect.Map:
fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, &out); err != nil {\n")
case reflect.Struct:
fmt.Fprint(sb, "\tif err := api.jsonCodec().Decode(data, out); err != nil {\n")
}
fmt.Fprint(sb, "\t\treturn nil, err\n")
fmt.Fprint(sb, "\t}\n")
switch d.ResponseTypeKind() {
case reflect.Map:
// For rationale, see https://play.golang.org/p/m9-MsTaQ5wt and
// https://play.golang.org/p/6h-v-PShMk9.
fmt.Fprint(sb, "\tif out == nil {\n")
fmt.Fprint(sb, "\t\treturn nil, ErrJSONLiteralNull\n")
fmt.Fprint(sb, "\t}\n")
case reflect.Struct:
// nothing
}
fmt.Fprintf(sb, "\treturn out, nil\n")
fmt.Fprintf(sb, "}\n\n")
}
// GenResponsesGo generates responses.go.
func GenResponsesGo(file string) {
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprint(&sb, "import (\n")
fmt.Fprint(&sb, "\t\"io\"\n")
fmt.Fprint(&sb, "\t\"io/ioutil\"\n")
fmt.Fprint(&sb, "\t\"net/http\"\n")
fmt.Fprint(&sb, "\n")
fmt.Fprint(&sb, "\t\"github.com/ooni/probe-cli/v3/internal/ooapi/apimodel\"\n")
fmt.Fprint(&sb, ")\n\n")
for _, desc := range Descriptors {
desc.genNewResponse(&sb)
}
writefile(file, &sb)
}

View file

@ -0,0 +1,136 @@
package main
import "github.com/ooni/probe-cli/v3/internal/ooapi/apimodel"
// URLPath describes a URLPath.
type URLPath struct {
// IsTemplate indicates whether Value contains a template. A future
// version of this implementation will automatically deduce that.
IsTemplate bool
// Value is the value of the URL path.
Value string
// InSwagger indicates the corresponding name to be used in
// the Swagger specification.
InSwagger string
}
// Descriptor is an API descriptor. It tells the generator
// what code it should emit for a given API.
type Descriptor struct {
// Name is the name of the API.
Name string
// CachePolicy indicates the caching policy to use.
CachePolicy int
// RequiresLogin indicates whether the API requires login.
RequiresLogin bool
// Method is the method to use ("GET" or "POST").
Method string
// URLPath is the URL path.
URLPath URLPath
// Request is an instance of the request type.
Request interface{}
// Response is an instance of the response type.
Response interface{}
}
// These are the caching policies.
const (
// CacheNone indicates we don't use a cache.
CacheNone = iota
// CacheFallback indicates we fallback to the cache
// when there is a failure.
CacheFallback
// CacheAlways indicates that we always check the
// cache before sending a request.
CacheAlways
)
// Descriptors describes all the APIs.
//
// Note that it matters whether the requests and responses
// are pointers. Generally speaking, if the message is a
// struct, use a pointer. If it's a map, don't.
var Descriptors = []Descriptor{{
Name: "CheckReportID",
Method: "GET",
URLPath: URLPath{Value: "/api/_/check_report_id"},
Request: &apimodel.CheckReportIDRequest{},
Response: &apimodel.CheckReportIDResponse{},
}, {
Name: "CheckIn",
Method: "POST",
URLPath: URLPath{Value: "/api/v1/check-in"},
Request: &apimodel.CheckInRequest{},
Response: &apimodel.CheckInResponse{},
}, {
Name: "Login",
Method: "POST",
URLPath: URLPath{Value: "/api/v1/login"},
Request: &apimodel.LoginRequest{},
Response: &apimodel.LoginResponse{},
}, {
Name: "MeasurementMeta",
Method: "GET",
URLPath: URLPath{Value: "/api/v1/measurement_meta"},
Request: &apimodel.MeasurementMetaRequest{},
Response: &apimodel.MeasurementMetaResponse{},
CachePolicy: CacheAlways,
}, {
Name: "Register",
Method: "POST",
URLPath: URLPath{Value: "/api/v1/register"},
Request: &apimodel.RegisterRequest{},
Response: &apimodel.RegisterResponse{},
}, {
Name: "TestHelpers",
Method: "GET",
URLPath: URLPath{Value: "/api/v1/test-helpers"},
Request: &apimodel.TestHelpersRequest{},
Response: apimodel.TestHelpersResponse{},
}, {
Name: "PsiphonConfig",
RequiresLogin: true,
Method: "GET",
URLPath: URLPath{Value: "/api/v1/test-list/psiphon-config"},
Request: &apimodel.PsiphonConfigRequest{},
Response: apimodel.PsiphonConfigResponse{},
}, {
Name: "TorTargets",
RequiresLogin: true,
Method: "GET",
URLPath: URLPath{Value: "/api/v1/test-list/tor-targets"},
Request: &apimodel.TorTargetsRequest{},
Response: apimodel.TorTargetsResponse{},
}, {
Name: "URLs",
Method: "GET",
URLPath: URLPath{Value: "/api/v1/test-list/urls"},
Request: &apimodel.URLsRequest{},
Response: &apimodel.URLsResponse{},
}, {
Name: "OpenReport",
Method: "POST",
URLPath: URLPath{Value: "/report"},
Request: &apimodel.OpenReportRequest{},
Response: &apimodel.OpenReportResponse{},
}, {
Name: "SubmitMeasurement",
Method: "POST",
URLPath: URLPath{
InSwagger: "/report/{report_id}",
IsTemplate: true,
Value: "/report/{{ .ReportID }}",
},
Request: &apimodel.SubmitMeasurementRequest{},
Response: &apimodel.SubmitMeasurementResponse{},
}}

View file

@ -0,0 +1,194 @@
package main
import (
"encoding/json"
"fmt"
"log"
"reflect"
"strings"
"sync"
"time"
"github.com/ooni/probe-cli/v3/internal/ooapi/internal/openapi"
)
const (
tagForJSON = "json"
tagForPath = "path"
)
func (d *Descriptor) genSwaggerURLPath() string {
up := d.URLPath
if up.InSwagger != "" {
return up.InSwagger
}
if up.IsTemplate {
panic("we should always use InSwapper and IsTemplate together")
}
return up.Value
}
func (d *Descriptor) genSwaggerSchema(cur reflect.Type) *openapi.Schema {
switch cur.Kind() {
case reflect.String:
return &openapi.Schema{Type: "string"}
case reflect.Bool:
return &openapi.Schema{Type: "boolean"}
case reflect.Int64:
return &openapi.Schema{Type: "integer"}
case reflect.Slice:
return &openapi.Schema{Type: "array", Items: d.genSwaggerSchema(cur.Elem())}
case reflect.Map:
return &openapi.Schema{Type: "object"}
case reflect.Ptr:
return d.genSwaggerSchema(cur.Elem())
case reflect.Struct:
if cur.String() == "time.Time" {
// Implementation note: we don't want to dive into time.Time but
// rather we want to pretend it's a string. The JSON parser for
// time.Time can indeed reconstruct a time.Time from a string, and
// it's much easier for us to let it do the parsing.
return &openapi.Schema{Type: "string"}
}
sinfo := &openapi.Schema{Type: "object"}
var once sync.Once
initmap := func() {
sinfo.Properties = make(map[string]*openapi.Schema)
}
for idx := 0; idx < cur.NumField(); idx++ {
field := cur.Field(idx)
if field.Tag.Get(tagForPath) != "" {
continue // skipping because this is a path param
}
if field.Tag.Get(tagForQuery) != "" {
continue // skipping because this is a query param
}
v := field.Name
if j := field.Tag.Get(tagForJSON); j != "" {
j = strings.Replace(j, ",omitempty", "", 1) // remove options
if j == "-" {
continue // not exported via JSON
}
v = j
}
once.Do(initmap)
sinfo.Properties[v] = d.genSwaggerSchema(field.Type)
}
return sinfo
case reflect.Interface:
return &openapi.Schema{Type: "object"}
default:
panic("unsupported type")
}
}
func (d *Descriptor) swaggerParamForType(t reflect.Type) string {
switch t.Kind() {
case reflect.String:
return "string"
case reflect.Bool:
return "boolean"
case reflect.Int64:
return "integer"
default:
panic("unsupported type")
}
}
func (d *Descriptor) genSwaggerParams(cur reflect.Type) []*openapi.Parameter {
// when we have params the input must be a pointer to struct
if cur.Kind() != reflect.Ptr {
panic("not a pointer")
}
cur = cur.Elem()
if cur.Kind() != reflect.Struct {
panic("not a pointer to struct")
}
// now that we're sure of the type, inspect the fields
var out []*openapi.Parameter
for idx := 0; idx < cur.NumField(); idx++ {
f := cur.Field(idx)
if q := f.Tag.Get(tagForQuery); q != "" {
out = append(
out, &openapi.Parameter{
Name: q,
In: "query",
Required: f.Tag.Get(tagForRequired) == "true",
Type: d.swaggerParamForType(f.Type),
})
continue
}
if p := f.Tag.Get(tagForPath); p != "" {
out = append(out, &openapi.Parameter{
Name: p,
In: "path",
Required: true,
Type: d.swaggerParamForType(f.Type),
})
continue
}
}
return out
}
func (d *Descriptor) genSwaggerPath() (string, *openapi.Path) {
pathStr, pathInfo := d.genSwaggerURLPath(), &openapi.Path{}
rtinfo := &openapi.RoundTrip{Produces: []string{"application/json"}}
switch d.Method {
case "GET":
pathInfo.Get = rtinfo
case "POST":
rtinfo.Consumes = append(rtinfo.Consumes, "application/json")
pathInfo.Post = rtinfo
default:
panic("unsupported method")
}
rtinfo.Parameters = d.genSwaggerParams(reflect.TypeOf(d.Request))
if d.Method != "GET" {
rtinfo.Parameters = append(rtinfo.Parameters, &openapi.Parameter{
Name: "body",
In: "body",
Required: true,
Schema: d.genSwaggerSchema(reflect.TypeOf(d.Request)),
})
}
rtinfo.Responses = &openapi.Responses{Successful: openapi.Body{
Description: "all good",
Schema: d.genSwaggerSchema(reflect.TypeOf(d.Response)),
}}
return pathStr, pathInfo
}
func genSwaggerVersion() string {
return time.Now().UTC().Format("0.20060102.1150405")
}
// GenSwaggerTestGo generates swagger_test.go
func GenSwaggerTestGo(file string) {
swagger := openapi.Swagger{
Swagger: "2.0",
Info: openapi.API{
Title: "OONI API specification",
Version: genSwaggerVersion(),
},
Host: "api.ooni.io",
BasePath: "/",
Schemes: []string{"https"},
Paths: make(map[string]*openapi.Path),
}
for _, desc := range Descriptors {
pathStr, pathInfo := desc.genSwaggerPath()
swagger.Paths[pathStr] = pathInfo
}
data, err := json.MarshalIndent(swagger, "", " ")
if err != nil {
log.Fatal(err)
}
var sb strings.Builder
fmt.Fprint(&sb, "// Code generated by go generate; DO NOT EDIT.\n")
fmt.Fprintf(&sb, "// %s\n\n", time.Now())
fmt.Fprint(&sb, "package ooapi\n\n")
fmt.Fprintf(&sb, "//go:generate go run ./internal/generator -file %s\n\n", file)
fmt.Fprintf(&sb, "const swagger = `%s`\n", string(data))
writefile(file, &sb)
}

View file

@ -0,0 +1,27 @@
package main
import (
"fmt"
"log"
"os"
"strings"
"golang.org/x/sys/execabs"
)
func writefile(name string, sb *strings.Builder) {
filep, err := os.Create(name)
if err != nil {
log.Fatal(err)
}
if _, err := fmt.Fprint(filep, sb.String()); err != nil {
log.Fatal(err)
}
if err := filep.Close(); err != nil {
log.Fatal(err)
}
cmd := execabs.Command("go", "fmt", name)
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
}

View file

@ -0,0 +1,64 @@
// Package openapi contains data structures for Swagger v2.0.
//
// We use these data structures to compare the API specification we
// have here with the one of the server.
package openapi
// Schema is the schema of a specific parameter or
// or the schema used by the response body
type Schema struct {
Properties map[string]*Schema `json:"properties,omitempty"`
Items *Schema `json:"items,omitempty"`
Type string `json:"type"`
}
// Parameter describes an input parameter, which could be in the
// URL path, in the query string, or in the request body
type Parameter struct {
In string `json:"in"`
Name string `json:"name"`
Required bool `json:"required,omitempty"`
Schema *Schema `json:"schema,omitempty"`
Type string `json:"type,omitempty"`
}
// Body describes a response body
type Body struct {
Description interface{} `json:"description,omitempty"`
Schema *Schema `json:"schema"`
}
// Responses describes the possible responses
type Responses struct {
Successful Body `json:"200"`
}
// RoundTrip describes an HTTP round trip with a given method and path
type RoundTrip struct {
Consumes []string `json:"consumes,omitempty"`
Produces []string `json:"produces,omitempty"`
Parameters []*Parameter `json:"parameters,omitempty"`
Responses *Responses `json:"responses,omitempty"`
}
// Path describes a path served by the API
type Path struct {
Get *RoundTrip `json:"get,omitempty"`
Post *RoundTrip `json:"post,omitempty"`
}
// API contains info about the API
type API struct {
Title string `json:"title"`
Version string `json:"version"`
}
// Swagger is the toplevel structure
type Swagger struct {
Swagger string `json:"swagger"`
Info API `json:"info"`
Host string `json:"host"`
BasePath string `json:"basePath"`
Schemes []string `json:"schemes"`
Paths map[string]*Path `json:"paths"`
}