Repository navigation
Expand file tree
/
Copy pathplugin_test.go
More file actions
175 lines (167 loc) · 6.53 KB
/
Copy pathplugin_test.go
File metadata and controls
175 lines (167 loc) · 6.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main
import (
"bufio"
"context"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
"strings"
"syscall"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/status"
"github.com/kryspal/plugin-sdk"
pluginv1 "github.com/kryspal/plugin-sdk/pluginv1"
)
// testToken is a made-up token of the right shape.
const testKey = "AbCdEf0123456789aBcDeF0123456789"
var han = regexp.MustCompile(`\p{Han}`)
func TestDescribe(t *testing.T) {
p := newPlugin()
for _, lang := range []string{"en", "zh-TW", "zh-CN", "", "ja"} {
m, err := p.Describe(context.Background(), &pluginv1.DescribeRequest{Language: lang})
if err != nil {
t.Fatal(err)
}
if m.GetId() != "assrt" || m.GetName() != "ASSRT" || m.GetDescription() == "" || m.GetVersion() != version() ||
!slices.Equal(m.GetCapabilities(), []string{pluginv1.CapabilitySubtitlesSearch}) ||
len(m.GetFeatures()) != 0 || m.GetSuggestedConcurrency() != 1 || len(m.GetConfigFields()) != 2 {
t.Errorf("Describe(%q) = %v", lang, m)
}
if !m.GetConfigFields()[0].GetRequired() || m.GetConfigFields()[0].GetType() != pluginv1.ConfigFieldType_CONFIG_FIELD_TYPE_SECRET {
t.Errorf("the token must be a required secret: %v", m.GetConfigFields()[0])
}
if m.GetAttribution().GetText() != attributionText || m.GetAttribution().GetLink() != "https://assrt.net" {
t.Errorf("attribution = %v", m.GetAttribution())
}
if (lang == "en" || lang == "" || lang == "ja") && han.MatchString(m.GetDescription()) {
t.Errorf("Describe(%q): Han characters in the English text", lang)
}
}
for _, tag := range messages.Tags() {
for key := range messageTables["en"] {
if messages.For(tag).T(key) == "" {
t.Errorf("%s: missing %s", tag, key)
}
}
}
if strings.ContainsAny(messageTables["zh-TW"]["desc"], "么识断读网为") {
t.Error("the zh-TW text has simplified characters")
}
if strings.ContainsAny(messageTables["zh-CN"]["desc"], "麼識斷讀網為") {
t.Error("the zh-CN text has traditional characters")
}
}
func TestConfigure(t *testing.T) {
p := newPlugin()
for name, c := range map[string]struct {
values map[string]string
field string // the field expected to error; empty means it passes
}{
"valid token": {map[string]string{keyToken: testKey}, ""},
"upper case": {map[string]string{keyToken: strings.ToUpper(testKey)}, ""},
"empty": {map[string]string{}, keyToken},
"short token": {map[string]string{keyToken: "abc123"}, keyToken},
"not alphanumeric": {map[string]string{keyToken: strings.Repeat("g", 31) + "-"}, keyToken},
"http base url": {map[string]string{keyToken: testKey, keyBaseURL: "http://proxy.example"}, keyBaseURL},
"https base url": {map[string]string{keyToken: testKey, keyBaseURL: "https://proxy.example/v1/"}, ""},
} {
resp, err := p.Configure(context.Background(), &pluginv1.ConfigureRequest{Values: c.values})
if err != nil {
t.Fatal(err)
}
if c.field == "" && len(resp.GetFieldErrors()) != 0 || c.field != "" && resp.GetFieldErrors()[c.field] == "" {
t.Errorf("%s: field errors = %v, want %q", name, resp.GetFieldErrors(), c.field)
}
}
_, _ = p.Configure(context.Background(), &pluginv1.ConfigureRequest{Values: map[string]string{keyToken: " " + testKey + " ", keyBaseURL: "https://proxy.example/v1/"}})
if k, b := p.settings(); k != testKey || b != "https://proxy.example/v1" {
t.Errorf("settings = %q %q", k, b)
}
}
// TestPluginProcess is the conformance check of the plugin process (the host's way): it compiles the executable and starts it with the host's environment
// variables, checking the handshake, health check, Describe, Search (no token is needs-configuration, no network) and graceful exit after SIGTERM.
func TestPluginProcess(t *testing.T) {
if testing.Short() {
t.Skip("builds and runs the plugin binary")
}
exe := filepath.Join(t.TempDir(), "assrt")
if out, err := exec.Command("go", "build", "-o", exe, ".").CombinedOutput(); err != nil {
t.Fatalf("build: %v\n%s", err, out)
}
sockDir, err := os.MkdirTemp("", "ass-")
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.RemoveAll(sockDir) }()
cmd := exec.Command(exe)
cmd.Env = []string{pluginapi.EnvCookie + "=" + pluginapi.CookieValue, pluginapi.EnvProtocols + "=1", pluginapi.EnvSocketDir + "=" + sockDir, "HOME=" + t.TempDir()}
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Fatal(err)
}
if err := cmd.Start(); err != nil {
t.Fatal(err)
}
stopped := false
defer func() {
if !stopped {
_ = cmd.Process.Kill()
_ = cmd.Wait()
}
}()
line, err := bufio.NewReader(stdout).ReadString('\n')
if err != nil {
t.Fatalf("handshake: %v", err)
}
hs, err := pluginapi.ParseHandshake(strings.TrimSpace(line))
if err != nil || hs.Protocol != 1 || hs.Network != pluginapi.NetworkUnix {
t.Fatalf("handshake %q: %+v, %v", line, hs, err)
}
conn, err := grpc.NewClient("unix:"+hs.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatal(err)
}
defer func() { _ = conn.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
hc := healthpb.NewHealthClient(conn)
for _, svc := range []string{"", pluginv1.HealthServiceSubtitleProvider} {
if r, err := hc.Check(ctx, &healthpb.HealthCheckRequest{Service: svc}); err != nil || r.GetStatus() != healthpb.HealthCheckResponse_SERVING {
t.Errorf("health %q = %v, %v", svc, r, err)
}
}
pc := pluginv1.NewPluginClient(conn)
m, err := pc.Describe(ctx, &pluginv1.DescribeRequest{Language: "zh-TW"})
if err != nil || m.GetId() != "assrt" || m.GetProtocolVersion() != pluginv1.ProtocolVersion {
t.Fatalf("Describe = %v, %v", m, err)
}
// Without a token the plugin needs configuration, and no network request is made.
if _, err := pluginv1.NewSubtitleProviderClient(conn).Search(ctx, &pluginv1.SearchSubtitlesRequest{}); status.Code(err) != codes.FailedPrecondition {
t.Errorf("Search without a token = %v, want FAILED_PRECONDITION", err)
}
if _, err := pluginv1.NewMetadataProviderClient(conn).Search(ctx, &pluginv1.SearchRequest{}); err == nil {
t.Error("the metadata service must not be registered")
}
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
t.Fatal(err)
}
stopped = true
done := make(chan error, 1)
go func() { done <- cmd.Wait() }()
select {
case err := <-done:
if err != nil {
t.Errorf("exit after SIGTERM: %v", err)
}
case <-time.After(15 * time.Second):
_ = cmd.Process.Kill()
t.Error("the plugin did not exit after SIGTERM")
}
}