From 342b3b343e521353c53e310f24d1fbc4543a91cf Mon Sep 17 00:00:00 2001 From: finn Date: Mon, 16 Oct 2023 15:10:31 -0700 Subject: [PATCH] Add more structure to the test list --- cmd/web5-spec-test/report-template.md | 12 +- cmd/web5-spec-test/report-template.txt | 5 +- cmd/web5-spec-test/reports.go | 2 +- openapi.yaml | 16 +++ openapi/openapi.go | 107 ++++++++++++++++++ sdks/web5-js/credentials.ts | 87 ++++++++------ sdks/web5-js/did-ion.ts | 17 +++ sdks/web5-js/main.ts | 50 ++++---- sdks/web5-js/openapi.d.ts | 15 +++ ...{credential_issuance.go => credentials.go} | 8 +- tests/did-ion.go | 61 ++++++++++ tests/{testsuite.go => test-runner.go} | 35 +++--- 12 files changed, 331 insertions(+), 84 deletions(-) create mode 100644 sdks/web5-js/did-ion.ts rename tests/{credential_issuance.go => credentials.go} (92%) create mode 100644 tests/did-ion.go rename tests/{testsuite.go => test-runner.go} (61%) diff --git a/cmd/web5-spec-test/report-template.md b/cmd/web5-spec-test/report-template.md index bb4389f..7888502 100644 --- a/cmd/web5-spec-test/report-template.md +++ b/cmd/web5-spec-test/report-template.md @@ -2,6 +2,12 @@ SDK: [{{ .TestServerID.Name }}]({{ .TestServerID.Url }}) ({{ .TestServerID.Language }}) -| Test | Pass | Details | -| ---- | ---- | ------- |{{ range $test, $result := .Results }} -| `{{ $test }}` | {{ if $result }}:x: | ```{{ $result }}```{{ else }}:heavy_check_mark: |{{ end }} |{{ end }} +{{ range $groupName, $results := .Results }} + +## {{ $groupName }} + +| Feature | Result | +| ------- | ------ |{{ range $test, $result := $results }} +| {{ $test }} | {{ if $result }}:x: {{ $result }}{{ else }}:heavy_check_mark:{{ end }} |{{ end }} + +{{ end }} diff --git a/cmd/web5-spec-test/report-template.txt b/cmd/web5-spec-test/report-template.txt index dbba619..0fc26cc 100644 --- a/cmd/web5-spec-test/report-template.txt +++ b/cmd/web5-spec-test/report-template.txt @@ -1,4 +1,7 @@ web5 spec conformance report for {{ .TestServerID.Name }} ({{ .TestServerID.Url }}) -{{ range $test, $result := .Results }} +{{ range $groupName, $results := .Results }} +{{ $groupName }} +======================{{ range $test, $result := $results }} {{ $test }}: {{ if $result }}fail: {{ $result }}{{ else }}pass{{ end }}{{ end }} +{{ end }} diff --git a/cmd/web5-spec-test/reports.go b/cmd/web5-spec-test/reports.go index 1dbea95..9a67fba 100644 --- a/cmd/web5-spec-test/reports.go +++ b/cmd/web5-spec-test/reports.go @@ -16,7 +16,7 @@ var templates = template.Must(template.New("").ParseFS(reportTemplateFS, "report type Report struct { TestServerID openapi.TestServerID - Results map[string]error + Results map[string]map[string]error } func (r Report) IsPassing() bool { diff --git a/openapi.yaml b/openapi.yaml index 3f546ea..93fa696 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3,6 +3,15 @@ info: title: web5 SDK test server version: 0.1.0 paths: + /did-ion/create: + post: + operationId: did_ion_create + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/DIDIonCreateResponse" /credentials/issue: post: operationId: credential_issue @@ -198,3 +207,10 @@ components: type: string url: type: string + DIDIonCreateResponse: + type: object + required: + - did + properties: + did: + type: string diff --git a/openapi/openapi.go b/openapi/openapi.go index 7d3f19d..3e07090 100644 --- a/openapi/openapi.go +++ b/openapi/openapi.go @@ -70,6 +70,11 @@ type CredentialStatus struct { // CredentialSubject defines model for CredentialSubject. type CredentialSubject map[string]interface{} +// DIDIonCreateResponse defines model for DIDIonCreateResponse. +type DIDIonCreateResponse struct { + Did string `json:"did"` +} + // TestServerID defines model for TestServerID. type TestServerID struct { Language string `json:"language"` @@ -173,6 +178,9 @@ type ClientInterface interface { CredentialIssue(ctx context.Context, body CredentialIssueJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // DidIonCreate request + DidIonCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // ServerReady request ServerReady(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -216,6 +224,18 @@ func (c *Client) CredentialIssue(ctx context.Context, body CredentialIssueJSONRe return c.Client.Do(req) } +func (c *Client) DidIonCreate(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDidIonCreateRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ServerReady(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewServerReadyRequest(c.Server) if err != nil { @@ -307,6 +327,33 @@ func NewCredentialIssueRequestWithBody(server string, contentType string, body i return req, nil } +// NewDidIonCreateRequest generates requests for DidIonCreate +func NewDidIonCreateRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/did-ion/create") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewServerReadyRequest generates requests for ServerReady func NewServerReadyRequest(server string) (*http.Request, error) { var err error @@ -412,6 +459,9 @@ type ClientWithResponsesInterface interface { CredentialIssueWithResponse(ctx context.Context, body CredentialIssueJSONRequestBody, reqEditors ...RequestEditorFn) (*CredentialIssueResponse, error) + // DidIonCreateWithResponse request + DidIonCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DidIonCreateResponse, error) + // ServerReadyWithResponse request ServerReadyWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ServerReadyResponse, error) @@ -463,6 +513,28 @@ func (r CredentialIssueResponse) StatusCode() int { return 0 } +type DidIonCreateResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DIDIonCreateResponse +} + +// Status returns HTTPResponse.Status +func (r DidIonCreateResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DidIonCreateResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type ServerReadyResponse struct { Body []byte HTTPResponse *http.Response @@ -531,6 +603,15 @@ func (c *ClientWithResponses) CredentialIssueWithResponse(ctx context.Context, b return ParseCredentialIssueResponse(rsp) } +// DidIonCreateWithResponse request returning *DidIonCreateResponse +func (c *ClientWithResponses) DidIonCreateWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*DidIonCreateResponse, error) { + rsp, err := c.DidIonCreate(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseDidIonCreateResponse(rsp) +} + // ServerReadyWithResponse request returning *ServerReadyResponse func (c *ClientWithResponses) ServerReadyWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ServerReadyResponse, error) { rsp, err := c.ServerReady(ctx, reqEditors...) @@ -601,6 +682,32 @@ func ParseCredentialIssueResponse(rsp *http.Response) (*CredentialIssueResponse, return response, nil } +// ParseDidIonCreateResponse parses an HTTP response from a DidIonCreateWithResponse call +func ParseDidIonCreateResponse(rsp *http.Response) (*DidIonCreateResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DidIonCreateResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DIDIonCreateResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseServerReadyResponse parses an HTTP response from a ServerReadyWithResponse call func ParseServerReadyResponse(rsp *http.Response) (*ServerReadyResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/sdks/web5-js/credentials.ts b/sdks/web5-js/credentials.ts index 4528562..b06c585 100644 --- a/sdks/web5-js/credentials.ts +++ b/sdks/web5-js/credentials.ts @@ -1,49 +1,62 @@ -import { Request, Response } from 'express'; -import { VcJwt, VerifiableCredential, SignOptions } from '@web5/credentials'; -import { DidKeyMethod, PortableDid } from '@web5/dids'; -import { Ed25519, Jose } from '@web5/crypto'; -import { paths } from './openapi.js'; +import { Request, Response } from "express"; +import { VcJwt, VerifiableCredential, SignOptions, CreateVcOptions } from "@web5/credentials"; +import { DidKeyMethod, PortableDid } from "@web5/dids"; +import { Ed25519, Jose } from "@web5/crypto"; +import { paths } from "./openapi.js"; type Signer = (data: Uint8Array) => Promise; let _ownDid: PortableDid; async function getOwnDid(): Promise { - if(_ownDid) { - return _ownDid; - } - _ownDid = await DidKeyMethod.create(); + if (_ownDid) { return _ownDid; + } + _ownDid = await DidKeyMethod.create(); + return _ownDid; } -export async function issueCredential(req: Request, res: Response) { - const body: paths["/credentials/issue"]["post"]["requestBody"]["content"]["application/json"] = req.body; - - const ownDid = await getOwnDid() - - // build signing options - const [signingKeyPair] = ownDid.keySet.verificationMethodKeys!; - const privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial; - const subjectIssuerDid = ownDid.did; - const signer = EdDsaSigner(privateKey); - const signOptions: SignOptions = { - issuerDid : ownDid.did, - subjectDid : ownDid.did, - kid : '#' + ownDid.did.split(':')[2], - signer : signer - }; - - const vcJwt: VcJwt = await VerifiableCredential.create(signOptions); - // const resp: paths["/credentials/issue"]["post"]["responses"]["200"]["content"]["application/json"] = { - // verifiableCredential: { - // }, - // } - res.json(vcJwt); +export async function credentialIssue(req: Request, res: Response) { + const body: paths["/credentials/issue"]["post"]["requestBody"]["content"]["application/json"] = + req.body; + + const ownDid = await getOwnDid(); + + // build signing options + const [signingKeyPair] = ownDid.keySet.verificationMethodKeys!; + const privateKey = ( + await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk! }) + ).keyMaterial; + const subjectIssuerDid = ownDid.did; + const signer = EdDsaSigner(privateKey); + const signOptions: SignOptions = { + issuerDid: ownDid.did, + subjectDid: ownDid.did, + kid: "#" + ownDid.did.split(":")[2], + signer: signer, + }; + + const createVcOptions: CreateVcOptions = { + credentialSubject: { id: "???" }, + issuer: { id: "issuer??" }, + }; + + const vcJwt: VcJwt = await VerifiableCredential.create( + signOptions, + createVcOptions + ); + +// const resp: paths["/credentials/issue"]["post"]["responses"]["200"]["content"]["application/json"] = { +// verifiableCredential: { +// }, +// } + + res.json(vcJwt); } function EdDsaSigner(privateKey: Uint8Array): Signer { - return async (data: Uint8Array): Promise => { - const signature = await Ed25519.sign({ data, key: privateKey}); - return signature; - }; - } \ No newline at end of file + return async (data: Uint8Array): Promise => { + const signature = await Ed25519.sign({ data, key: privateKey }); + return signature; + }; +} diff --git a/sdks/web5-js/did-ion.ts b/sdks/web5-js/did-ion.ts new file mode 100644 index 0000000..c7d68b3 --- /dev/null +++ b/sdks/web5-js/did-ion.ts @@ -0,0 +1,17 @@ +import { DidIonMethod } from "@web5/dids"; +import { paths } from "./openapi.js"; +import { Request, Response } from "express"; + + +export async function didIonCreate(req: Request, res: Response) { + // const body: paths["/did-ion/create"]["post"]["requestBody"]["content"]["application/json"] = + // req.body; + const did = await DidIonMethod.create({}); + + const resp: paths["/did-ion/create"]["post"]["responses"]["200"]["content"]["application/json"] = + { + did: did.did, + }; + + res.json(resp); +} diff --git a/sdks/web5-js/main.ts b/sdks/web5-js/main.ts index 5d61c29..f156747 100644 --- a/sdks/web5-js/main.ts +++ b/sdks/web5-js/main.ts @@ -1,40 +1,44 @@ -import express from 'express'; -import { issueCredential } from './credentials.js'; -import type * as http from 'http'; -import type { Request, Response } from 'express' -import { paths } from './openapi.js' // generated with npx openapi-typescript .web5-component/openapi.yaml -o .web5-component/openapi.d.ts -import bodyparser from 'body-parser'; +import express from "express"; +import { credentialIssue } from "./credentials.js"; +import { didIonCreate } from "./did-ion.js"; +import type * as http from "http"; +import type { Request, Response } from "express"; +import { paths } from "./openapi.js"; // generated with npx openapi-typescript .web5-component/openapi.yaml -o .web5-component/openapi.d.ts +import bodyparser from "body-parser"; const app: express.Application = express(); app.use(express.json()); app.use(bodyparser.json()); -app.post("/credentials/issue", issueCredential); +app.post("/did-ion/create", didIonCreate); -const serverID: paths["/"]["get"]["responses"]["200"]["content"]["application/json"] = { +app.post("/credentials/issue", credentialIssue); + +const serverID: paths["/"]["get"]["responses"]["200"]["content"]["application/json"] = + { name: "web5-js", language: "JavaScript", url: "https://github.com/TBD54566975/web5-js", -} + }; app.get("/", (req, res) => { - res.json(serverID); + res.json(serverID); }); let server: http.Server; app.get("/shutdown", (req: Request, res: Response) => { - res.send("ok"); - console.log("shutting down server"); - server.close((e) => { - if(e) { - console.error(e); - } - }); + res.send("ok"); + console.log("shutting down server"); + server.close((e) => { + if (e) { + console.error(e); + } + }); }); server = app.listen(8080, () => console.log("test server started")); -process.on('SIGTERM', () => { - console.log('SIGTERM signal received: closing HTTP server') - server.close(() => { - console.log('HTTP server closed') - }) - }) \ No newline at end of file +process.on("SIGTERM", () => { + console.log("SIGTERM signal received: closing HTTP server"); + server.close(() => { + console.log("HTTP server closed"); + }); +}); diff --git a/sdks/web5-js/openapi.d.ts b/sdks/web5-js/openapi.d.ts index de3d982..a638cef 100644 --- a/sdks/web5-js/openapi.d.ts +++ b/sdks/web5-js/openapi.d.ts @@ -5,6 +5,9 @@ export interface paths { + "/did-ion/create": { + post: operations["did_ion_create"]; + }; "/credentials/issue": { post: operations["credential_issue"]; }; @@ -80,6 +83,9 @@ export interface components { language: string; url: string; }; + DIDIonCreateResponse: { + did: string; + }; }; responses: never; parameters: never; @@ -94,6 +100,15 @@ export type external = Record; export interface operations { + did_ion_create: { + responses: { + 200: { + content: { + "application/json": components["schemas"]["DIDIonCreateResponse"]; + }; + }; + }; + }; credential_issue: { requestBody: { content: { diff --git a/tests/credential_issuance.go b/tests/credentials.go similarity index 92% rename from tests/credential_issuance.go rename to tests/credentials.go index 5a093bd..160c921 100644 --- a/tests/credential_issuance.go +++ b/tests/credentials.go @@ -8,7 +8,13 @@ import ( "github.com/TBD54566975/web5-spec/openapi" ) -func CredentialIssuanceTest(ctx context.Context, serverURL string) error { +func init() { + tests["Credentials SDK"] = map[string]testfn{ + "VC Create": vcCreate, + } +} + +func vcCreate(ctx context.Context, serverURL string) error { expectedContext := []string{"https://www.w3.org/2018/credentials/v1"} expectedType := []string{"VerifiableCredential"} expectedID := "id-123" diff --git a/tests/did-ion.go b/tests/did-ion.go new file mode 100644 index 0000000..ee0a87e --- /dev/null +++ b/tests/did-ion.go @@ -0,0 +1,61 @@ +package tests + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/TBD54566975/web5-spec/openapi" +) + +func init() { + tests["DID ION"] = map[string]testfn{ + "CreateRequest": didIonCreateRequest, + // "UpdateRequest": didIonUpdateRequest, + // "RecoverRequest": didIonRecoverRequest, + // "DeactivateRequest": didIonDeactivateRequest, + // "Resolution": didIonResolution, + // "Anchoring": didIonAnchoring, + } +} + +func didIonCreateRequest(ctx context.Context, serverURL string) error { + client, err := openapi.NewClientWithResponses(serverURL) + if err != nil { + return err + } + + response, err := client.DidIonCreateWithResponse(ctx) + if err != nil { + return err + } + + if response.JSON200 == nil { + return fmt.Errorf("%s: %s", response.Status(), string(response.Body)) + } + + didParts := strings.Split(response.JSON200.Did, ":") + if len(didParts) != 4 { + return fmt.Errorf("invalid did:ion returned: 4 parts expected, %d found: %s", len(didParts), didParts) + } + + errs := []error{} + if err := compareStrings(didParts[0], "did", "did 1st part"); err != nil { + errs = append(errs, err) + } + + if err := compareStrings(didParts[1], "ion", "did 2nd part"); err != nil { + errs = append(errs, err) + } + + if len(didParts[2]) != 46 { + errs = append(errs, fmt.Errorf("3rd part of returned did of unexpected length: expected 46 characters, got %d", len(didParts[2]))) + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} diff --git a/tests/testsuite.go b/tests/test-runner.go similarity index 61% rename from tests/testsuite.go rename to tests/test-runner.go index d6b128d..d2f6f9e 100644 --- a/tests/testsuite.go +++ b/tests/test-runner.go @@ -8,24 +8,23 @@ import ( "golang.org/x/exp/slog" ) -type test struct { - Name string - Fn func(ctx context.Context, serverURL string) error -} - -var tests = []test{ - {Name: "CredentialIssuance", Fn: CredentialIssuanceTest}, -} - -func RunTests(serverURL string) map[string]error { - results := map[string]error{} - for _, t := range tests { - slog.Info("running", "test", t.Name) - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - results[t.Name] = t.Fn(ctx, serverURL) - if results[t.Name] != nil { - slog.Error("error", "test", t.Name, "error", results[t.Name]) +type testfn func(ctx context.Context, serverURL string) error + +var tests = map[string]map[string]testfn{} + +func RunTests(serverURL string) map[string]map[string]error { + results := map[string]map[string]error{} + for group, ts := range tests { + slog.Info("starting test group", "group", group) + results[group] = map[string]error{} + for t, fn := range ts { + slog.Info("running", "test", t) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + results[group][t] = fn(ctx, serverURL) + if results[t] != nil { + slog.Error("error", "test", t, "error", results[t]) + } } }