308 lines
8.0 KiB
Go
308 lines
8.0 KiB
Go
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.uber.org/multierr"
|
|
|
|
"go.sour.is/ev"
|
|
"go.sour.is/ev/app/webfinger"
|
|
memstore "go.sour.is/ev/pkg/es/driver/mem-store"
|
|
"go.sour.is/ev/pkg/es/driver/projecter"
|
|
"go.sour.is/ev/pkg/es/driver/streamer"
|
|
"go.sour.is/ev/pkg/es/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{
|
|
Rel: "salty:public",
|
|
Type: "application/json+salty",
|
|
},
|
|
&webfinger.LinkSet{
|
|
Rel: "salty:private",
|
|
Type: "application/json+salty",
|
|
},
|
|
&webfinger.LinkSet{
|
|
Rel: "salty:public",
|
|
Type: "application/json+salty",
|
|
HRef: "https://ev.sour.is/inbox/01GAEMKXYJ4857JQP1MJGD61Z5",
|
|
Properties: map[string]*string{
|
|
"pub": ptr("kex1r8zshlvkc787pxvauaq7hd6awa9kmheddxjj9k80qmenyxk6284s50uvpw"),
|
|
},
|
|
},
|
|
&webfinger.LinkDeleted{
|
|
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)
|
|
|
|
// fmt.Println(base64.RawURLEncoding.EncodeToString(key))
|
|
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()
|
|
}
|