128 lines
4.2 KiB
Go
128 lines
4.2 KiB
Go
|
|
package behaviour
|
||
|
|
|
||
|
|
import (
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"testing"
|
||
|
|
)
|
||
|
|
|
||
|
|
// openAPIDocument is a minimal but real OpenAPI 3.1 description, used both
|
||
|
|
// pasted into the console and served over HTTP for the scan-a-URL flow.
|
||
|
|
const openAPIDocument = `{
|
||
|
|
"openapi": "3.1.0",
|
||
|
|
"info": {"title": "Guess API", "version": "1.0.0"},
|
||
|
|
"paths": {
|
||
|
|
"/health": {
|
||
|
|
"get": {
|
||
|
|
"operationId": "health",
|
||
|
|
"summary": "Liveness probe",
|
||
|
|
"responses": {"200": {"description": "ok"}}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"/guesses": {
|
||
|
|
"get": {
|
||
|
|
"operationId": "listGuesses",
|
||
|
|
"summary": "List guesses",
|
||
|
|
"responses": {"200": {"description": "ok"}}
|
||
|
|
},
|
||
|
|
"post": {
|
||
|
|
"operationId": "createGuess",
|
||
|
|
"summary": "Create a guess",
|
||
|
|
"responses": {"201": {"description": "created"}}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}`
|
||
|
|
|
||
|
|
// contractPhases imports a service contract from a pasted document and from a
|
||
|
|
// scanned URL, which is the path an operator takes when registering an
|
||
|
|
// application.
|
||
|
|
func contractPhases(t *testing.T, j *journey) {
|
||
|
|
t.Helper()
|
||
|
|
host := j.adminHost()
|
||
|
|
|
||
|
|
phase(t, "a pasted contract is imported with its endpoints", func(t *testing.T) {
|
||
|
|
type saved struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Version string `json:"version"`
|
||
|
|
EndpointCount int `json:"endpoint_count"`
|
||
|
|
}
|
||
|
|
response := j.admin.json(http.MethodPost, host, "/api/v1/contracts", map[string]any{
|
||
|
|
"application_id": j.applicationID, "source_type": "document", "document": openAPIDocument,
|
||
|
|
})
|
||
|
|
contract := decode[saved](t, response, http.StatusCreated)
|
||
|
|
if contract.Version != "3.1.0" {
|
||
|
|
t.Errorf("version = %q", contract.Version)
|
||
|
|
}
|
||
|
|
if contract.EndpointCount != 3 {
|
||
|
|
t.Errorf("endpoint_count = %d, want 3", contract.EndpointCount)
|
||
|
|
}
|
||
|
|
j.contractID = contract.ID
|
||
|
|
|
||
|
|
// The endpoints themselves must be queryable, since the console lists them.
|
||
|
|
var scanned int
|
||
|
|
if err := j.h.db.QueryRowContext(t.Context(),
|
||
|
|
`SELECT count(*) FROM contract_endpoints WHERE contract_id = $1`, contract.ID).Scan(&scanned); err != nil {
|
||
|
|
t.Fatalf("count contract endpoints: %v", err)
|
||
|
|
}
|
||
|
|
if scanned != 3 {
|
||
|
|
t.Errorf("stored endpoints = %d, want 3", scanned)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
phase(t, "a malformed contract is refused with a readable error", func(t *testing.T) {
|
||
|
|
response := j.admin.json(http.MethodPost, host, "/api/v1/contracts", map[string]any{
|
||
|
|
"application_id": j.applicationID, "source_type": "document", "document": `{"openapi":`,
|
||
|
|
})
|
||
|
|
body := statusOnly(t, response, http.StatusUnprocessableEntity)
|
||
|
|
if body == "" {
|
||
|
|
t.Error("no explanation returned")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
phase(t, "an empty contract is refused", func(t *testing.T) {
|
||
|
|
response := j.admin.json(http.MethodPost, host, "/api/v1/contracts", map[string]any{
|
||
|
|
"application_id": j.applicationID, "source_type": "document", "document": "",
|
||
|
|
})
|
||
|
|
statusOnly(t, response, http.StatusBadRequest)
|
||
|
|
})
|
||
|
|
|
||
|
|
phase(t, "a contract is scanned from a URL", func(t *testing.T) {
|
||
|
|
source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
|
|
w.Header().Set("Content-Type", "application/json")
|
||
|
|
_, _ = w.Write([]byte(openAPIDocument))
|
||
|
|
}))
|
||
|
|
defer source.Close()
|
||
|
|
|
||
|
|
type scanned struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
SourceURL string `json:"source_url"`
|
||
|
|
}
|
||
|
|
response := j.admin.json(http.MethodPost, host, "/api/v1/contracts", map[string]any{
|
||
|
|
"application_id": j.applicationID, "source_type": "url", "source_url": source.URL + "/openapi.json",
|
||
|
|
})
|
||
|
|
result := decode[scanned](t, response, http.StatusCreated)
|
||
|
|
if result.SourceURL == "" {
|
||
|
|
t.Error("the source URL was not recorded")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
phase(t, "an unreachable contract URL reports a gateway error", func(t *testing.T) {
|
||
|
|
response := j.admin.json(http.MethodPost, host, "/api/v1/contracts", map[string]any{
|
||
|
|
"application_id": j.applicationID, "source_type": "url",
|
||
|
|
"source_url": "http://127.0.0.1:1/openapi.json",
|
||
|
|
})
|
||
|
|
statusOnly(t, response, http.StatusBadGateway)
|
||
|
|
})
|
||
|
|
|
||
|
|
phase(t, "the dashboard reports the stored contracts", func(t *testing.T) {
|
||
|
|
type listing struct {
|
||
|
|
Contracts []map[string]any `json:"contracts"`
|
||
|
|
}
|
||
|
|
result := decode[listing](t, j.admin.get(host, "/api/v1/dashboard", nil), http.StatusOK)
|
||
|
|
if len(result.Contracts) < 2 {
|
||
|
|
t.Errorf("contracts = %d, want the pasted and scanned documents", len(result.Contracts))
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|