go-tools/app/webfinger/jrd_test.go

311 lines
8.0 KiB
Go
Raw Normal View History

2023-09-29 10:31:25 -06:00
package webfinger_test
import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
jwt "github.com/golang-jwt/jwt/v4"
"github.com/matryer/is"
"go.sour.is/ev"
"go.uber.org/multierr"
"go.sour.is/tools/app/webfinger"
memstore "go.sour.is/ev/driver/mem-store"
"go.sour.is/ev/driver/projecter"
"go.sour.is/ev/driver/streamer"
"go.sour.is/ev/event"
)
func TestParseJRD(t *testing.T) {
// Adapted from spec http://tools.ietf.org/html/rfc6415#appendix-A
blob := `
{
"subject":"http://blog.example.com/article/id/314",
"aliases":[
"http://blog.example.com/cool_new_thing",
"http://blog.example.com/steve/article/7"],
"properties":{
"http://blgx.example.net/ns/version":"1.3",
"http://blgx.example.net/ns/ext":null
},
"links":[
{
"rel":"author",
"type":"text/html",
"href":"http://blog.example.com/author/steve",
"titles":{
"default":"About the Author",
"en-us":"Author Information"
},
"properties":{
"http://example.com/role":"editor"
}
},
{
"rel":"author",
"href":"http://example.com/author/john",
"titles":{
"default":"The other author"
}
},
{
"rel":"copyright"
}
]
}
`
obj, err := webfinger.ParseJRD([]byte(blob))
if err != nil {
t.Fatal(err)
}
if got, want := obj.Subject, "http://blog.example.com/article/id/314"; got != want {
t.Errorf("JRD.Subject is %q, want %q", got, want)
}
if got, want := obj.GetProperty("http://blgx.example.net/ns/version"), "1.3"; got != want {
t.Errorf("obj.GetProperty('http://blgx.example.net/ns/version') returned %q, want %q", got, want)
}
if got, want := obj.GetProperty("http://blgx.example.net/ns/ext"), ""; got != want {
t.Errorf("obj.GetProperty('http://blgx.example.net/ns/ext') returned %q, want %q", got, want)
}
if obj.GetLinkByRel("copyright") == nil {
t.Error("obj.GetLinkByRel('copyright') returned nil, want non-nil value")
}
if got, want := obj.GetLinkByRel("author").Titles["default"], "About the Author"; got != want {
t.Errorf("obj.GetLinkByRel('author').Titles['default'] returned %q, want %q", got, want)
}
if got, want := obj.GetLinkByRel("author").GetProperty("http://example.com/role"), "editor"; got != want {
t.Errorf("obj.GetLinkByRel('author').GetProperty('http://example.com/role') returned %q, want %q", got, want)
}
}
func TestEncodeJRD(t *testing.T) {
s, err := json.Marshal(&webfinger.JRD{
Subject: "test",
Properties: map[string]*string{
"https://sour.is/ns/prop1": nil,
},
})
if err != nil {
t.Fatal(err)
}
if string(s) != `{"subject":"test","properties":{"https://sour.is/ns/prop1":null}}` {
t.Fatal("output does not match")
}
}
func TestApplyEvents(t *testing.T) {
is := is.New(t)
events := event.NewEvents(
&webfinger.SubjectSet{
Subject: "acct:me@sour.is",
Properties: map[string]*string{
"https://sour.is/ns/pubkey": ptr("kex1d330ama4vnu3vll5dgwjv3k0pcxsccc5k2xy3j8khndggkszsmsq3hl4ru"),
},
},
&webfinger.LinkSet{
Index: 0,
Rel: "salty:public",
Type: "application/json+salty",
},
&webfinger.LinkSet{
Index: 1,
Rel: "salty:private",
Type: "application/json+salty",
},
&webfinger.LinkSet{
Index: 0,
Rel: "salty:public",
Type: "application/json+salty",
HRef: "https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5",
Properties: map[string]*string{
"pub": ptr("kex1r8zshlvkc787pxvauaq7hd6awa9kmheddxjj9k80qmenyxk6284s50uvpw"),
},
},
&webfinger.LinkDeleted{
Index: 1,
Rel: "salty:private",
},
)
event.SetStreamID(webfinger.StreamID("acct:me@sour.is"), events...)
jrd := &webfinger.JRD{}
jrd.ApplyEvent(events...)
s, err := json.Marshal(jrd)
if err != nil {
t.Fatal(err)
}
is.Equal(string(s), `{"subject":"acct:me@sour.is","properties":{"https://sour.is/ns/pubkey":"kex1d330ama4vnu3vll5dgwjv3k0pcxsccc5k2xy3j8khndggkszsmsq3hl4ru"},"links":[{"rel":"salty:public","type":"application/json+salty","href":"https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5","properties":{"pub":"kex1r8zshlvkc787pxvauaq7hd6awa9kmheddxjj9k80qmenyxk6284s50uvpw"}}]}`)
events = event.NewEvents(
&webfinger.SubjectDeleted{},
)
event.SetStreamID(webfinger.StreamID("acct:me@sour.is"), events...)
jrd.ApplyEvent(events...)
s, err = json.Marshal(jrd)
if err != nil {
t.Fatal(err)
}
t.Log(string(s))
if string(s) != `{}` {
t.Fatal("output does not match")
}
}
func TestCommands(t *testing.T) {
is := is.New(t)
ctx := context.Background()
pub, priv, err := ed25519.GenerateKey(nil)
is.NoErr(err)
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
"sub": "acct:me@sour.is",
"pub": enc(pub),
"aliases": []string{"acct:xuu@sour.is"},
"properties": map[string]*string{
"https://example.com/ns/asdf": nil,
webfinger.NSpubkey: ptr(enc(pub)),
},
"links": []map[string]any{{
"rel": "salty:public",
"type": "application/json+salty",
"href": "https://ev.sour.is",
"titles": map[string]string{"default": "Jon Lundy"},
"properties": map[string]*string{
"pub": ptr("kex140fwaena9t0mrgnjeare5zuknmmvl0vc7agqy5yr938vusxfh9ys34vd2p"),
},
}},
"exp": time.Now().Add(30 * time.Second).Unix(),
})
aToken, err := token.SignedString(priv)
is.NoErr(err)
es, err := ev.Open(ctx, "mem:", streamer.New(ctx), projecter.New(ctx))
is.NoErr(err)
type claims struct {
Subject string `json:"sub"`
PubKey string `json:"pub"`
*webfinger.JRD
jwt.StandardClaims
}
token, err = jwt.ParseWithClaims(
aToken,
&claims{},
func(tok *jwt.Token) (any, error) {
c, ok := tok.Claims.(*claims)
if !ok {
return nil, fmt.Errorf("wrong type of claim")
}
c.JRD.Subject = c.Subject
c.StandardClaims.Subject = c.Subject
c.SetProperty(webfinger.NSpubkey, &c.PubKey)
pub, err := dec(c.PubKey)
return ed25519.PublicKey(pub), err
},
jwt.WithValidMethods([]string{"EdDSA"}),
jwt.WithJSONNumber(),
)
is.NoErr(err)
c, ok := token.Claims.(*claims)
is.True(ok)
t.Logf("%#v", c)
a, err := ev.Upsert(ctx, es, webfinger.StreamID(c.Subject), func(ctx context.Context, a *webfinger.JRD) error {
var auth *webfinger.JRD
// does the target have a pubkey for self auth?
if _, ok := a.Properties[webfinger.NSpubkey]; ok {
auth = a
}
// Check current version for auth.
if authID, ok := a.Properties[webfinger.NSauth]; ok && authID != nil && auth == nil {
auth = &webfinger.JRD{}
auth.SetStreamID(webfinger.StreamID(*authID))
err := es.Load(ctx, auth)
if err != nil {
return err
}
}
if a.Version() == 0 || a.IsDeleted() {
// else does the new object claim auth from another object?
if authID, ok := c.Properties[webfinger.NSauth]; ok && authID != nil && auth == nil {
auth = &webfinger.JRD{}
auth.SetStreamID(webfinger.StreamID(*authID))
err := es.Load(ctx, auth)
if err != nil {
return err
}
}
// fall back to use auth from submitted claims
if auth == nil {
auth = c.JRD
}
}
if auth == nil {
return fmt.Errorf("auth not found")
}
err = a.OnAuth(c.JRD, auth)
if err != nil {
return err
}
return a.OnClaims(c.JRD)
})
is.NoErr(err)
for _, e := range a.Events(false) {
t.Log(e)
}
}
func ptr[T any](v T) *T {
return &v
}
func enc(b []byte) string {
return base64.RawURLEncoding.EncodeToString(b)
}
func dec(s string) ([]byte, error) {
s = strings.TrimSpace(s)
return base64.RawURLEncoding.DecodeString(s)
}
func TestMain(m *testing.M) {
ctx, stop := context.WithCancel(context.Background())
defer stop()
err := multierr.Combine(
ev.Init(ctx),
event.Init(ctx),
memstore.Init(ctx),
)
if err != nil {
fmt.Println(err)
return
}
m.Run()
}