operator-channel: the state asks typed to the router's projection; mesh-node-enroller: redeem's answer typed, with a fixture and a client test (ADR 0634 §6; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
package meshnodeenrollerclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
|
||||
)
|
||||
|
||||
func TestATokenLifeAndTheRedeemProofAreAsTheHolderMakesThem(t *testing.T) {
|
||||
@@ -28,3 +33,26 @@ func TestATokenLifeAndTheRedeemProofAreAsTheHolderMakesThem(t *testing.T) {
|
||||
t.Errorf("SignedBytes = %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTheRedeemAnswerIsTheContractsWire: the client's answer reads into the contract's type with no field dropped, and
|
||||
// the contract's reads back as the client's wire.
|
||||
func TestTheRedeemAnswerIsTheContractsWire(t *testing.T) {
|
||||
mine := RedeemAnswer{Node: "g14", Login: "g14-login", Account: "YQ==", Certificate: "pem", Signer: "dg==", Signature: "cw=="}
|
||||
wire, _ := json.Marshal(mine)
|
||||
var theirs seat.RedeemAnswer
|
||||
d := json.NewDecoder(bytes.NewReader(wire))
|
||||
d.DisallowUnknownFields()
|
||||
if err := d.Decode(&theirs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if theirs.Node != "g14" || theirs.Signer != "dg==" || theirs.Signature != "cw==" {
|
||||
t.Errorf("the contract reads %+v", theirs)
|
||||
}
|
||||
back, _ := json.Marshal(theirs)
|
||||
var a, b map[string]string
|
||||
_ = json.Unmarshal(wire, &a)
|
||||
_ = json.Unmarshal(back, &b)
|
||||
if len(a) != 6 || !reflect.DeepEqual(a, b) {
|
||||
t.Errorf("the contract reads %s back as %s", wire, back)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,20 @@ type RedeemInput struct {
|
||||
}
|
||||
|
||||
// RedeemAnswer is what redeem answers.
|
||||
type RedeemAnswer = map[string]any
|
||||
type RedeemAnswer struct {
|
||||
// its account sealed to the sealing key it presented, base64
|
||||
Account string `json:"account"`
|
||||
// its certificate, PEM
|
||||
Certificate string `json:"certificate"`
|
||||
// the login it dials as next
|
||||
Login string `json:"login"`
|
||||
// the node
|
||||
Node string `json:"node"`
|
||||
// the vault's signature over the answer without it, under the context of the enrolment kind, base64
|
||||
Signature string `json:"signature,omitempty"`
|
||||
// the vault's public signing half, base64
|
||||
Signer string `json:"signer"`
|
||||
}
|
||||
|
||||
// ConfirmInput is what confirm takes.
|
||||
type ConfirmInput struct {
|
||||
|
||||
@@ -31,6 +31,8 @@ func TestFixtures(t *testing.T) {
|
||||
err := json.Unmarshal(b, &v)
|
||||
return v, err
|
||||
}, subjectIs: nil},
|
||||
{name: "redeem answers the account sealed to the node, signed by the vault", subject: "", given: "{\"account\":\"c2VhbGVk\",\"certificate\":\"-----BEGIN CERTIFICATE-----\\nMIIB\\n-----END CERTIFICATE-----\\n\",\"login\":\"g14-login\",\"node\":\"g14\",\"signature\":\"c2ln\",\"signer\":\"dmF1bHQ=\"}", wire: "{\"account\":\"c2VhbGVk\",\"certificate\":\"-----BEGIN CERTIFICATE-----\\nMIIB\\n-----END CERTIFICATE-----\\n\",\"login\":\"g14-login\",\"node\":\"g14\",\"signature\":\"c2ln\",\"signer\":\"dmF1bHQ=\"}",
|
||||
decode: func(b []byte) (any, error) { var v RedeemAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
}
|
||||
for _, c := range cases {
|
||||
v, err := c.decode([]byte(c.given))
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"name": "redeem answers the account sealed to the node, signed by the vault",
|
||||
"of": "verb:redeem.answer",
|
||||
"given": {
|
||||
"node": "g14",
|
||||
"login": "g14-login",
|
||||
"account": "c2VhbGVk",
|
||||
"certificate": "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n",
|
||||
"signer": "dmF1bHQ=",
|
||||
"signature": "c2ln"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -90,7 +90,40 @@
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the account sealed to the node, its certificate and the vault's public half, signed by the vault"
|
||||
"description": "the account sealed to the node, its certificate and the vault's public half, signed by the vault",
|
||||
"properties": {
|
||||
"node": {
|
||||
"type": "string",
|
||||
"description": "the node"
|
||||
},
|
||||
"login": {
|
||||
"type": "string",
|
||||
"description": "the login it dials as next"
|
||||
},
|
||||
"account": {
|
||||
"type": "string",
|
||||
"description": "its account sealed to the sealing key it presented, base64"
|
||||
},
|
||||
"certificate": {
|
||||
"type": "string",
|
||||
"description": "its certificate, PEM"
|
||||
},
|
||||
"signer": {
|
||||
"type": "string",
|
||||
"description": "the vault's public signing half, base64"
|
||||
},
|
||||
"signature": {
|
||||
"type": "string",
|
||||
"description": "the vault's signature over the answer without it, under the context of the enrolment kind, base64"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node",
|
||||
"login",
|
||||
"account",
|
||||
"certificate",
|
||||
"signer"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -123,7 +123,20 @@ export interface RedeemInput {
|
||||
}
|
||||
|
||||
/** is what redeem answers. */
|
||||
export type RedeemAnswer = Record<string, unknown>;
|
||||
export interface RedeemAnswer {
|
||||
/** its account sealed to the sealing key it presented, base64 */
|
||||
"account": string;
|
||||
/** its certificate, PEM */
|
||||
"certificate": string;
|
||||
/** the login it dials as next */
|
||||
"login": string;
|
||||
/** the node */
|
||||
"node": string;
|
||||
/** the vault's signature over the answer without it, under the context of the enrolment kind, base64 */
|
||||
"signature"?: string;
|
||||
/** the vault's public signing half, base64 */
|
||||
"signer": string;
|
||||
}
|
||||
|
||||
/** is what confirm takes. */
|
||||
export interface ConfirmInput {
|
||||
|
||||
@@ -477,4 +477,23 @@ const (
|
||||
)
|
||||
|
||||
// AsksByAskerIDValue is one value of the state asks under the key <asker>.<id>.
|
||||
type AsksByAskerIDValue = map[string]any
|
||||
type AsksByAskerIDValue struct {
|
||||
// how it ended, in words
|
||||
Answer string `json:"answer,omitempty"`
|
||||
// its asker
|
||||
Asker string `json:"asker"`
|
||||
// its asker has been told how it ended
|
||||
AskerTold bool `json:"asker_told,omitempty"`
|
||||
// when it ends unanswered, RFC 3339
|
||||
Expires string `json:"expires"`
|
||||
// its headline
|
||||
Headline string `json:"headline"`
|
||||
// <asker>.<id>
|
||||
Key string `json:"key"`
|
||||
// the channels it was offered on
|
||||
OfferedOn []string `json:"offered_on,omitempty"`
|
||||
// when it was opened, RFC 3339
|
||||
Opened string `json:"opened"`
|
||||
// how it stands: open, chosen, expired, cancelled, replaced or refused
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
@@ -182,7 +182,56 @@
|
||||
"key": "<asker>.<id>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "one ask the router holds and how it stands"
|
||||
"description": "one ask the router holds and how it stands, as the router's tool states it",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "<asker>.<id>"
|
||||
},
|
||||
"asker": {
|
||||
"type": "string",
|
||||
"description": "its asker"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string",
|
||||
"description": "its headline"
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "how it stands: open, chosen, expired, cancelled, replaced or refused"
|
||||
},
|
||||
"opened": {
|
||||
"type": "string",
|
||||
"description": "when it was opened, RFC 3339"
|
||||
},
|
||||
"expires": {
|
||||
"type": "string",
|
||||
"description": "when it ends unanswered, RFC 3339"
|
||||
},
|
||||
"offered_on": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "the channels it was offered on"
|
||||
},
|
||||
"asker_told": {
|
||||
"type": "boolean",
|
||||
"description": "its asker has been told how it ended"
|
||||
},
|
||||
"answer": {
|
||||
"type": "string",
|
||||
"description": "how it ended, in words"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"asker",
|
||||
"headline",
|
||||
"state",
|
||||
"opened",
|
||||
"expires"
|
||||
]
|
||||
},
|
||||
"description": "one ask the router holds and how it stands"
|
||||
}
|
||||
|
||||
@@ -406,5 +406,24 @@ export interface ClosedHolderEvent {
|
||||
}
|
||||
|
||||
/** is one value of the state asks under the key <asker>.<id>. */
|
||||
export type AsksByAskerIDValue = Record<string, unknown>;
|
||||
export interface AsksByAskerIDValue {
|
||||
/** how it ended, in words */
|
||||
"answer"?: string;
|
||||
/** its asker */
|
||||
"asker": string;
|
||||
/** its asker has been told how it ended */
|
||||
"asker_told"?: boolean;
|
||||
/** when it ends unanswered, RFC 3339 */
|
||||
"expires": string;
|
||||
/** its headline */
|
||||
"headline": string;
|
||||
/** <asker>.<id> */
|
||||
"key": string;
|
||||
/** the channels it was offered on */
|
||||
"offered_on"?: string[];
|
||||
/** when it was opened, RFC 3339 */
|
||||
"opened": string;
|
||||
/** how it stands: open, chosen, expired, cancelled, replaced or refused */
|
||||
"state": string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user