222 lines
7.6 KiB
Go
222 lines
7.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// fakeBus is the work queue and the state: it records each ask and lets a test write the answer, as the converter
|
|
// process would under the ask's id.
|
|
type fakeBus struct {
|
|
mu sync.Mutex
|
|
asks []Ask
|
|
answers map[string]json.RawMessage
|
|
answerOn func(Ask) json.RawMessage // answers an ask as it is published
|
|
}
|
|
|
|
func (b *fakeBus) Publish(id string, body any) error {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
raw, _ := json.Marshal(body)
|
|
var a Ask
|
|
_ = json.Unmarshal(raw, &a)
|
|
b.asks = append(b.asks, a)
|
|
if b.answerOn != nil {
|
|
if ans := b.answerOn(a); ans != nil {
|
|
b.answers[id] = ans
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *fakeBus) Answer(id string) (json.RawMessage, error) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return b.answers[id], nil
|
|
}
|
|
|
|
func rig(wait time.Duration) (*Service, *fakeBus) {
|
|
b := &fakeBus{answers: map[string]json.RawMessage{}}
|
|
return &Service{Asks: b, Now: time.Now, Wait: wait, Poll: time.Millisecond}, b
|
|
}
|
|
|
|
const get, put = "http://novox.internal:20001/b/doc.docx?X-Amz-Signature=a", "http://novox.internal:20001/b/doc.pdf?X-Amz-Signature=b"
|
|
|
|
// 1595/AC1: one ask with those fields is published on convert-asks, and the verb answers what the converter wrote under
|
|
// the ask's id.
|
|
func TestConvertQueuesOneAskAndAnswersWhatTheConverterWrote(t *testing.T) {
|
|
s, b := rig(time.Second)
|
|
b.answerOn = func(a Ask) json.RawMessage { return json.RawMessage(`{"size": 4096, "sha256": "ab12"}`) }
|
|
got, err := s.Convert(context.Background(), get, "DOCX", put, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(b.asks) != 1 {
|
|
t.Fatalf("%d asks queued", len(b.asks))
|
|
}
|
|
a := b.asks[0]
|
|
if a.Op != OpConvert || a.Source != get || a.Type != "docx" || a.Target != put || a.ID == "" {
|
|
t.Fatalf("ask %+v", a)
|
|
}
|
|
if got["size"] != float64(4096) || got["sha256"] != "ab12" || got["ask"] != a.ID {
|
|
t.Fatalf("answer %v", got)
|
|
}
|
|
}
|
|
|
|
// 1595/AC1: an error the converter wrote is the verb's error.
|
|
func TestConvertAnswersTheConvertersError(t *testing.T) {
|
|
s, b := rig(time.Second)
|
|
b.answerOn = func(a Ask) json.RawMessage {
|
|
return json.RawMessage(`{"error": "-4: the document server could not download the file"}`)
|
|
}
|
|
if _, err := s.Convert(context.Background(), get, "doc", put, ""); err == nil || !strings.Contains(err.Error(), "-4: the document server") {
|
|
t.Fatalf("error %v", err)
|
|
}
|
|
}
|
|
|
|
// 1595/AC2: a type outside the twelve is refused naming the types taken, and nothing is queued.
|
|
func TestConvertRefusesOtherTypes(t *testing.T) {
|
|
s, b := rig(time.Second)
|
|
for _, typ := range []string{"exe", "pdf", "", "docm", "jpg"} {
|
|
_, err := s.Convert(context.Background(), get, typ, put, "")
|
|
if err == nil || !strings.Contains(err.Error(), "docx") || !strings.Contains(err.Error(), "txt") {
|
|
t.Errorf("type %q: %v", typ, err)
|
|
}
|
|
}
|
|
if _, err := s.Convert(context.Background(), "file:///etc/passwd", "docx", put, ""); err == nil {
|
|
t.Error("a file link was taken")
|
|
}
|
|
if len(b.asks) != 0 {
|
|
t.Fatalf("%d asks queued", len(b.asks))
|
|
}
|
|
if len(Types) != 12 {
|
|
t.Fatalf("%d types", len(Types))
|
|
}
|
|
}
|
|
|
|
// 1595/AC3, AC6: with no answer within the call's bound the verb answers that it is converting, with the ask id,
|
|
// and does not fail; convert {ask} then returns the converter's answer once it is written, or converting again.
|
|
func TestAConversionNotDoneInTimeIsAnsweredAsConvertingAndRead(t *testing.T) {
|
|
s, b := rig(30 * time.Millisecond)
|
|
got, err := s.Convert(context.Background(), get, "docx", put, "")
|
|
if err != nil {
|
|
t.Fatalf("a slow converter failed the call: %v", err)
|
|
}
|
|
id := b.asks[0].ID
|
|
if got["state"] != "converting" || got["ask"] != id {
|
|
t.Fatalf("answer %v", got)
|
|
}
|
|
again, err := s.Read(context.Background(), id)
|
|
if err != nil || again["state"] != "converting" || again["ask"] != id {
|
|
t.Fatalf("read while converting: %v, %v", again, err)
|
|
}
|
|
b.mu.Lock()
|
|
b.answers[id] = json.RawMessage(`{"size": 77, "sha256": "cd34"}`)
|
|
b.mu.Unlock()
|
|
done, err := s.Read(context.Background(), id)
|
|
if err != nil || done["size"] != float64(77) || done["sha256"] != "cd34" || done["state"] != nil {
|
|
t.Fatalf("read after: %v, %v", done, err)
|
|
}
|
|
if len(b.asks) != 1 {
|
|
t.Errorf("%d asks queued, want the one", len(b.asks))
|
|
}
|
|
if ConvertWait != 25*time.Second {
|
|
t.Errorf("a call waits %s, want 25s", ConvertWait)
|
|
}
|
|
// Through the tool: ask alone reads; ask with anything else is refused; a made-up ask id is refused.
|
|
tl := tools(s)[0]
|
|
if r, err := tl.Run(map[string]any{"ask": id}); err != nil || r.(map[string]any)["size"] != float64(77) {
|
|
t.Errorf("tool read: %v, %v", r, err)
|
|
}
|
|
if _, err := tl.Run(map[string]any{"ask": id, "source": get}); err == nil {
|
|
t.Error("ask with a source was accepted")
|
|
}
|
|
if _, err := tl.Run(map[string]any{"ask": "../x"}); err == nil {
|
|
t.Error("a made-up ask id was accepted")
|
|
}
|
|
}
|
|
|
|
// 1595/AC4: the tool side opens no credential: its code never names the JWT or a secret file, and the manifest's
|
|
// tools bundle is given no env naming one.
|
|
func TestTheToolSideHoldsNoCredential(t *testing.T) {
|
|
// The tool side is these two files; the converter process (issue 1596) is code of its own, which alone reads the token.
|
|
for _, f := range []string{"main.go", "convert.go"} {
|
|
raw, err := os.ReadFile(f)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, word := range []string{"jwt", "JWT", ".secret", "os.ReadFile", "os.Open"} {
|
|
if strings.Contains(string(raw), word) {
|
|
t.Errorf("%s mentions %q", f, word)
|
|
}
|
|
}
|
|
}
|
|
m := manifest(t)
|
|
for _, a := range m["build"].(map[string]any)["artifacts"].([]any) {
|
|
if env, ok := a.(map[string]any)["env"]; ok {
|
|
t.Errorf("an artifact is given env %v", env)
|
|
}
|
|
}
|
|
}
|
|
|
|
func manifest(t *testing.T) map[string]any {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m map[string]any
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// 1595/AC5: only-office claims document-converter (scope mesh) serving convert, declares the stream convert-asks as a
|
|
// work queue and the state convert-answers (the shape of brother's device-asks and device-answers).
|
|
func TestManifestClaimsTheSeat(t *testing.T) {
|
|
m := manifest(t)
|
|
seats, _ := m["seats"].([]any)
|
|
if len(seats) != 1 || seats[0].(map[string]any)["name"] != "document-converter" || seats[0].(map[string]any)["scope"] != "mesh" ||
|
|
len(seats[0].(map[string]any)["serves"].([]any)) != 1 || seats[0].(map[string]any)["serves"].([]any)[0] != "convert" {
|
|
t.Fatalf("the seat document-converter is not declared in seats (the mesh defines no seat a module does not declare): %v", seats)
|
|
}
|
|
claims := m["claims"].([]any)
|
|
c := claims[0].(map[string]any)
|
|
if len(claims) != 1 || c["name"] != "document-converter" || c["scope"] != "mesh" ||
|
|
len(c["serves"].([]any)) != 1 || c["serves"].([]any)[0] != "convert" {
|
|
t.Fatalf("claims %v", claims)
|
|
}
|
|
streams := m["streams"].([]any)
|
|
s := streams[0].(map[string]any)
|
|
if len(streams) != 1 || s["name"] != "convert-asks" || s["retention"] != "work-queue" || s["max-deliveries"] != float64(3) ||
|
|
s["events"].([]any)[0] != EventAsked {
|
|
t.Fatalf("streams %v", streams)
|
|
}
|
|
state := m["state"].([]any)
|
|
st := state[0].(map[string]any)
|
|
if len(state) != 1 || st["name"] != AnswersState || st["ttl-seconds"] != float64(600) {
|
|
t.Fatalf("state %v", state)
|
|
}
|
|
found := false
|
|
for _, a := range m["build"].(map[string]any)["artifacts"].([]any) {
|
|
found = found || a.(map[string]any)["from"] == "cmd/only-office"
|
|
}
|
|
if !found {
|
|
t.Error("no Go bundle")
|
|
}
|
|
}
|
|
|
|
func TestTheVerbIsListedUnderTheSeat(t *testing.T) {
|
|
s, _ := rig(time.Second)
|
|
ts := tools(s)
|
|
if len(ts) != 1 || ts[0].Name != "document-converter.convert" {
|
|
t.Fatalf("tools %v", ts)
|
|
}
|
|
}
|