|
|
|
internal/gates/jobs_test.go raw
| 1 | package gates | | 2 | | | 3 | import ( | | 4 | "context" | | 5 | "database/sql" | | 6 | "encoding/json" | | 7 | "strings" | | 8 | "testing" | | 9 | | | 10 | "github.com/FelineStateMachine/tinyrelay/internal/community" | | 11 | "github.com/FelineStateMachine/tinyrelay/internal/event" | | 12 | "github.com/FelineStateMachine/tinyrelay/internal/policy" | | 13 | "github.com/FelineStateMachine/tinyrelay/internal/relay" | | 14 | "github.com/FelineStateMachine/tinyrelay/internal/storage" | | 15 | ) | | 16 | | | 17 | const jobMemberSecret = "0000000000000000000000000000000000000000000000000000000000000005" | | 18 | | | 19 | // jobFixture adds a human member who requests jobs to the agent fixture. | | 20 | type jobFixture struct { | | 21 | agentFixture | | 22 | member string | | 23 | } | | 24 | | | 25 | func newJobFixture(t *testing.T) jobFixture { | | 26 | t.Helper() | | 27 | f := newAgentFixture(t) | | 28 | member, _ := event.PublicKey(jobMemberSecret) | | 29 | raw, _ := json.Marshal(member) | | 30 | if _, err := f.community.Execute(context.Background(), f.owner, "setmember" , []json.RawMessage{raw}); err != nil { | | 31 | t.Fatal(err) | | 32 | } | | 33 | return jobFixture{agentFixture: f, member: member} | | 34 | } | | 35 | | | 36 | // request publishes a job request from the member and stores it, so | | 37 | // answers can name a request the relay holds. | | 38 | func (f jobFixture) request(t *testing.T, kind int, createdAt int64) event.Event { | | 39 | t.Helper() | | 40 | ctx := context.Background() | | 41 | e := signed(t, jobMemberSecret, kind, createdAt, [][]string{{"i" , "hello" , "text" }, {"output" , "text/plain" }, {"bid" , "1000" }}, "" ) | | 42 | if err := f.gate.Write(ctx, e, relay.Session{PubKeys: []string{f.member}}, agentNow); err != nil { | | 43 | t.Fatalf("member request rejected: %v" , err) | | 44 | } | | 45 | if err := f.store.WithTx(ctx, func(tx *sql.Tx) error { | | 46 | _, err := storage.SaveTx(ctx, tx, e, storage.SaveOptions{Now: agentNow}) | | 47 | return err | | 48 | }); err != nil { | | 49 | t.Fatal(err) | | 50 | } | | 51 | return e | | 52 | } | | 53 | | | 54 | func expectError(t *testing.T, err error, prefix, fragment string) { | | 55 | t.Helper() | | 56 | if err == nil || !strings.HasPrefix(err.Error(), prefix) || !strings.Contains(err.Error(), fragment) { | | 57 | t.Fatalf("want %s error containing %q, got %v" , prefix, fragment, err) | | 58 | } | | 59 | } | | 60 | | | 61 | func TestJobShapeRefusesMalformedEvents(t *testing.T) { | | 62 | f := newJobFixture(t) | | 63 | ctx := context.Background() | | 64 | session := relay.Session{PubKeys: []string{f.member}} | | 65 | request := f.request(t, 5001, agentNow) | | 66 | cases := []struct { | | 67 | name string | | 68 | kind int | | 69 | tags [][]string | | 70 | fragment string | | 71 | }{ | | 72 | {"input without a type" , 5001, [][]string{{"i" , "hello" }}, "url, event, job or text" }, | | 73 | {"input with an unknown type" , 5001, [][]string{{"i" , "hello" , "voice" }}, "url, event, job or text" }, | | 74 | {"event input without an id" , 5001, [][]string{{"i" , "abc" , "event" }}, "event id" }, | | 75 | {"encrypted without a provider" , 5001, [][]string{{"encrypted" }}, "p tag" }, | | 76 | {"bid that is not an amount" , 5001, [][]string{{"bid" , "ten" }}, "millisats" }, | | 77 | {"result without a request" , 6001, [][]string{{"p" , f.member}}, "e tag" }, | | 78 | {"result without a requester" , 6001, [][]string{{"e" , request.ID}}, "p tag" }, | | 79 | {"result with a bad amount" , 6001, [][]string{{"e" , request.ID}, {"p" , f.member}, {"amount" , "-1" }}, "millisats" }, | | 80 | {"result of the wrong kind" , 6002, [][]string{{"e" , request.ID}, {"p" , f.member}}, "does not answer" }, | | 81 | {"result naming the wrong requester" , 6001, [][]string{{"e" , request.ID}, {"p" , f.owner}}, "name the requester" }, | | 82 | {"feedback without a status" , 7000, [][]string{{"e" , request.ID}, {"p" , f.member}}, "payment-required, processing, error, success or partial" }, | | 83 | {"feedback with an unknown status" , 7000, [][]string{{"e" , request.ID}, {"p" , f.member}, {"status" , "done" }}, "payment-required, processing, error, success or partial" }, | | 84 | } | | 85 | for _, tc := range cases { | | 86 | err := f.gate.Write(ctx, signed(t, jobMemberSecret, tc.kind, agentNow, tc.tags, "" ), session, agentNow) | | 87 | expectError(t, err, "invalid:" , tc.fragment) | | 88 | } | | 89 | // Well-formed answers from a member pass, including answers to a request | | 90 | // made on another relay. | | 91 | for _, e := range []event.Event{ | | 92 | signed(t, jobMemberSecret, 6001, agentNow, [][]string{{"e" , request.ID}, {"p" , f.member}, {"amount" , "500" , "lnbc1" }}, "output" ), | | 93 | signed(t, jobMemberSecret, 7000, agentNow, [][]string{{"e" , request.ID}, {"p" , f.member}, {"status" , "processing" }}, "" ), | | 94 | signed(t, jobMemberSecret, 6005, agentNow, [][]string{{"e" , strings.Repeat("a" , 64)}, {"p" , f.owner}}, "elsewhere" ), | | 95 | } { | | 96 | if err := f.gate.Write(ctx, e, session, agentNow); err != nil { | | 97 | t.Fatalf("well-formed kind %d rejected: %v" , e.Kind, err) | | 98 | } | | 99 | } | | 100 | // Import applies the same shape rules. | | 101 | expectError(t, f.gate.Import(ctx, signed(t, jobMemberSecret, 7000, agentNow, [][]string{{"e" , request.ID}, {"p" , f.member}, {"status" , "done" }}, "" ), agentNow), "invalid:" , "payment-required" ) | | 102 | } | | 103 | | | 104 | func TestJobPolicyAndMembership(t *testing.T) { | | 105 | f := newJobFixture(t) | | 106 | ctx := context.Background() | | 107 | other, _ := event.PublicKey(agentOtherSecret) | | 108 | request := signed(t, agentOtherSecret, 5001, agentNow, [][]string{{"i" , "hello" , "text" }}, "" ) | | 109 | // The relay writes openly, but job kinds are for members and agents. | | 110 | expectError(t, f.gate.Write(ctx, request, relay.Session{PubKeys: []string{other}}, agentNow), "restricted:" , "members and agents" ) | | 111 | if err := f.gate.Write(ctx, signed(t, agentOtherSecret, 1, agentNow, nil, "note" ), relay.Session{PubKeys: []string{other}}, agentNow); err != nil { | | 112 | t.Fatalf("open relay refused a note: %v" , err) | | 113 | } | | 114 | f.request(t, 5001, agentNow) | | 115 | if err := f.gate.Write(ctx, signed(t, agentOwnerSecret, 5002, agentNow, nil, "" ), relay.Session{PubKeys: []string{f.owner}}, agentNow); err != nil { | | 116 | t.Fatalf("owner request rejected: %v" , err) | | 117 | } | | 118 | // Switching long tasks off refuses every job kind, for members and on | | 119 | // import alike. | | 120 | p := f.gate.cfg.Policy() | | 121 | p.Features.Jobs = false | | 122 | f.gate.cfg.Policy = func() policy.Policy { return p } | | 123 | expectError(t, f.gate.Write(ctx, signed(t, jobMemberSecret, 5001, agentNow+1, nil, "" ), relay.Session{PubKeys: []string{f.member}}, agentNow), "restricted:" , "switched off" ) | | 124 | expectError(t, f.gate.Import(ctx, signed(t, jobMemberSecret, 7000, agentNow+1, [][]string{{"e" , strings.Repeat("b" , 64)}, {"p" , f.member}, {"status" , "success" }}, "" ), agentNow), "restricted:" , "switched off" ) | | 125 | if err := f.gate.Write(ctx, signed(t, jobMemberSecret, 1, agentNow+1, nil, "still fine" ), relay.Session{PubKeys: []string{f.member}}, agentNow); err != nil { | | 126 | t.Fatalf("switching jobs off broke notes: %v" , err) | | 127 | } | | 128 | } | | 129 | | | 130 | func TestJobGrantsRequestServeAndBoth(t *testing.T) { | | 131 | f := newJobFixture(t) | | 132 | ctx := context.Background() | | 133 | session := relay.Session{PubKeys: []string{f.agent}} | | 134 | request := f.request(t, 5001, agentNow) | | 135 | answer := func(kind int, extra ...[]string) event.Event { | | 136 | tags := append([][]string{{"e" , request.ID}, {"p" , f.member}}, extra...) | | 137 | return signed(t, agentSecret, kind, agentNow, tags, "output" ) | | 138 | } | | 139 | | | 140 | // request: 5xxx only. | | 141 | f.grant(t, agentNow, grantTags(f.agent, agentNow+3600, []string{"jobs" , "request" })) | | 142 | if err := f.gate.Write(ctx, signed(t, agentSecret, 5001, agentNow, [][]string{{"i" , "hello" , "text" }}, "" ), session, agentNow); err != nil { | | 143 | t.Fatalf("request grant refused a request: %v" , err) | | 144 | } | | 145 | expectRestricted(t, f.gate.Write(ctx, answer(6001), session, agentNow), "kind 6001" ) | | 146 | expectRestricted(t, f.gate.Write(ctx, answer(7000, []string{"status" , "processing" }), session, agentNow), "kind 7000" ) | | 147 | | | 148 | // serve: 6xxx and 7000 in reply to a held request; no requests. | | 149 | f.grant(t, agentNow+1, grantTags(f.agent, agentNow+3600, []string{"jobs" , "serve" })) | | 150 | expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 5001, agentNow, nil, "" ), session, agentNow), "kind 5001" ) | | 151 | if err := f.gate.Write(ctx, answer(6001, []string{"amount" , "500" }), session, agentNow); err != nil { | | 152 | t.Fatalf("serve grant refused a result: %v" , err) | | 153 | } | | 154 | if err := f.gate.Write(ctx, answer(7000, []string{"status" , "payment-required" }, []string{"amount" , "500" , "lnbc1" }), session, agentNow); err != nil { | | 155 | t.Fatalf("serve grant refused feedback: %v" , err) | | 156 | } | | 157 | unknown := signed(t, agentSecret, 6001, agentNow, [][]string{{"e" , strings.Repeat("c" , 64)}, {"p" , f.member}}, "" ) | | 158 | expectRestricted(t, f.gate.Write(ctx, unknown, session, agentNow), "requests this relay holds" ) | | 159 | expectRestricted(t, f.gate.Import(ctx, unknown, agentNow), "requests this relay holds" ) | | 160 | // A note that is not a request cannot be answered, and an answer must | | 161 | // match the request's kind and author. | | 162 | note := signed(t, jobMemberSecret, 1, agentNow, nil, "not a job" ) | | 163 | if err := f.store.WithTx(ctx, func(tx *sql.Tx) error { | | 164 | _, err := storage.SaveTx(ctx, tx, note, storage.SaveOptions{Now: agentNow}) | | 165 | return err | | 166 | }); err != nil { | | 167 | t.Fatal(err) | | 168 | } | | 169 | expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 6001, agentNow, [][]string{{"e" , note.ID}, {"p" , f.member}}, "" ), session, agentNow), "requests this relay holds" ) | | 170 | expectError(t, f.gate.Write(ctx, answer(6002), session, agentNow), "invalid:" , "does not answer" ) | | 171 | expectError(t, f.gate.Write(ctx, signed(t, agentSecret, 6001, agentNow, [][]string{{"e" , request.ID}, {"p" , f.owner}}, "" ), session, agentNow), "invalid:" , "name the requester" ) | | 172 | | | 173 | // both: requests and answers. | | 174 | f.grant(t, agentNow+2, grantTags(f.agent, agentNow+3600, []string{"jobs" , "both" })) | | 175 | if err := f.gate.Write(ctx, signed(t, agentSecret, 5001, agentNow, nil, "" ), session, agentNow); err != nil { | | 176 | t.Fatalf("both grant refused a request: %v" , err) | | 177 | } | | 178 | if err := f.gate.Write(ctx, answer(6001), session, agentNow); err != nil { | | 179 | t.Fatalf("both grant refused a result: %v" , err) | | 180 | } | | 181 | // A k tag still covers a single job kind, and answers from a k grant | | 182 | // name a held request as well. | | 183 | f.grant(t, agentNow+3, grantTags(f.agent, agentNow+3600, []string{"k" , "5001" }, []string{"k" , "6001" })) | | 184 | if err := f.gate.Write(ctx, signed(t, agentSecret, 5001, agentNow, nil, "" ), session, agentNow); err != nil { | | 185 | t.Fatalf("k grant refused a request: %v" , err) | | 186 | } | | 187 | expectRestricted(t, f.gate.Write(ctx, signed(t, agentSecret, 5002, agentNow, nil, "" ), session, agentNow), "kind 5002" ) | | 188 | if err := f.gate.Write(ctx, answer(6001), session, agentNow); err != nil { | | 189 | t.Fatalf("k grant refused a result: %v" , err) | | 190 | } | | 191 | expectRestricted(t, f.gate.Write(ctx, unknown, session, agentNow), "requests this relay holds" ) | | 192 | // The jobs tag is bounded. | | 193 | if _, err := community.ParseAgentGrant(signed(t, agentOwnerSecret, event.KIND_AGENT_GRANT, agentNow, grantTags(f.agent, agentNow+3600, []string{"jobs" , "all" }), "" ), agentNow); err == nil || !strings.Contains(err.Error(), "request, serve or both" ) { | | 194 | t.Fatalf("jobs tag accepted an unknown value: %v" , err) | | 195 | } | | 196 | grant, err := community.ParseAgentGrant(signed(t, agentOwnerSecret, event.KIND_AGENT_GRANT, agentNow, grantTags(f.agent, agentNow+3600, []string{"jobs" , "serve" }), "" ), agentNow) | | 197 | if err != nil || grant.Scope.Jobs != community.AgentJobsServe || !grant.ServesJobs() || grant.RequestsJobs() { | | 198 | t.Fatalf("serve grant parsed as %+v: %v" , grant.Scope, err) | | 199 | } | | 200 | } |
|
|
About| owner | c01503e8035f |
|---|
| visibility | public |
|---|
| head | d730679639bf |
|---|
Maintainers- c01503e8035f owner
- ba1b5beed5b5 agent hermes
Clonehttps://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git
|