Extract AppAPI auth and OCS preferences from CheckDNS.
Publish gonexapp v1 with Credentials, OCSClient, and parameterized AppAPIPreferences so ExApps share Nextcloud ExApp plumbing per ADR 0013. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# go-nc-exapp
|
||||||
|
|
||||||
|
Shared Go Library for Nextcloud ExApp Services: AppAPI authentication, OCS calls, and per-user ExApp preferences. ExApps import `gitea.neitzel.de/konrad/go-nc-exapp`. File storage and folder visits live in go-nc-files.
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
**AppAPI credentials**:
|
||||||
|
The ExApp's shared secret and Nextcloud base URL, plus optional per-request user identity. Used to sign outbound calls to Nextcloud and to read the requesting user from inbound AppAPI-proxied requests.
|
||||||
|
_Avoid_: API key (generic), session token
|
||||||
|
|
||||||
|
**Requesting user**:
|
||||||
|
The Nextcloud user on whose behalf the current ExApp request runs, taken from AppAPI authorization headers. WebDAV and preferences use this user; there is no separate ExApp login.
|
||||||
|
_Avoid_: service account (for per-request identity), anonymous
|
||||||
|
|
||||||
|
**ExApp preference**:
|
||||||
|
A string value stored in Nextcloud for one user and one ExApp, keyed by the ExApp (not admin AppConfig). Libraries expose a parameterized key; each ExApp chooses its own key names.
|
||||||
|
_Avoid_: settings file in User Files, instance-wide config
|
||||||
|
|
||||||
|
**OCS**:
|
||||||
|
Nextcloud's legacy HTTP API surface under `/ocs/v2.php/…`. This Library requests JSON responses (`format=json`) for machine-readable bodies.
|
||||||
|
_Avoid_: assuming XML responses, REST-only Nextcloud APIs for ExApp prefs
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# go-nc-exapp
|
||||||
|
|
||||||
|
Shared Go library for Nextcloud ExApp Services: AppAPI authentication, OCS JSON calls, and per-user ExApp preferences.
|
||||||
|
|
||||||
|
Import: `gitea.neitzel.de/konrad/go-nc-exapp`
|
||||||
|
|
||||||
|
## v1 scope
|
||||||
|
|
||||||
|
**Included**
|
||||||
|
|
||||||
|
- **Credentials** — Nextcloud base URL, AppAPI secret, and requesting user identity
|
||||||
|
- **AuthHeaders** — outbound AppAPI authorization for OCS and other Nextcloud calls
|
||||||
|
- **WithUser** — credentials scoped to a specific requesting user
|
||||||
|
- **UserFromRequest** — extract the requesting user from inbound AppAPI-proxied requests
|
||||||
|
- **OCSClient** — authenticated OCS calls that always append `format=json`
|
||||||
|
- **AppAPIPreferences** — parameterized get/set of a string ExApp preference (caller supplies app id and key)
|
||||||
|
|
||||||
|
**Excluded from v1**
|
||||||
|
|
||||||
|
- ExApp lifecycle HTTP routes (`/heartbeat`, `/enabled`, …)
|
||||||
|
- HaRP listen / `serve()` and unix-socket bootstrap
|
||||||
|
- Top-menu, script, and iframe UI registration
|
||||||
|
- WebDAV and file storage (see **go-nc-files**)
|
||||||
|
- **Visit** folder resolution (see **go-nc-files**)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
cred := gonexapp.Credentials{
|
||||||
|
BaseURL: "https://nextcloud.example",
|
||||||
|
AppID: "myexapp",
|
||||||
|
AppVersion: "0.1.0",
|
||||||
|
AAVersion: "1.0.0",
|
||||||
|
AppSecret: os.Getenv("APP_SECRET"),
|
||||||
|
UserID: "alice",
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs := gonexapp.NewAppAPIPreferences(cred, "myexapp", "savedDefault")
|
||||||
|
value, err := prefs.Get()
|
||||||
|
```
|
||||||
|
|
||||||
|
Each ExApp chooses its own preference keys; this library does not hardcode product-specific names.
|
||||||
|
|
||||||
|
## Domain language
|
||||||
|
|
||||||
|
See [CONTEXT.md](./CONTEXT.md) for AppAPI credentials, requesting user, ExApp preference, and OCS terminology.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- **go-nc-files** — WebDAV, Working Folder, Saved Default, Visit resolution
|
||||||
|
- Workspace ADR 0013 — extraction from CheckDNS
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package gonexapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Credentials are what an ExApp needs to call Nextcloud as a user.
|
||||||
|
type Credentials struct {
|
||||||
|
BaseURL string // Nextcloud instance URL, no trailing slash
|
||||||
|
AppID string
|
||||||
|
AppVersion string
|
||||||
|
AAVersion string
|
||||||
|
AppSecret string
|
||||||
|
UserID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthHeaders returns AppAPI auth headers for requests to Nextcloud.
|
||||||
|
func (c Credentials) AuthHeaders() http.Header {
|
||||||
|
h := make(http.Header)
|
||||||
|
h.Set("AA-VERSION", c.AAVersion)
|
||||||
|
h.Set("EX-APP-ID", c.AppID)
|
||||||
|
h.Set("EX-APP-VERSION", c.AppVersion)
|
||||||
|
token := base64.StdEncoding.EncodeToString([]byte(c.UserID + ":" + c.AppSecret))
|
||||||
|
h.Set("AUTHORIZATION-APP-API", token)
|
||||||
|
h.Set("OCS-APIRequest", "true")
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUser returns a copy acting as userID.
|
||||||
|
func (c Credentials) WithUser(userID string) Credentials {
|
||||||
|
out := c
|
||||||
|
out.UserID = userID
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserFromRequest reads the requesting user from AUTHORIZATION-APP-API on an inbound ExApp request.
|
||||||
|
func UserFromRequest(r *http.Request) (string, error) {
|
||||||
|
raw := r.Header.Get("AUTHORIZATION-APP-API")
|
||||||
|
if raw == "" {
|
||||||
|
return "", fmt.Errorf("missing AUTHORIZATION-APP-API")
|
||||||
|
}
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(raw)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("invalid AUTHORIZATION-APP-API: %w", err)
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(string(decoded), ":", 2)
|
||||||
|
if len(parts) < 1 || strings.TrimSpace(parts[0]) == "" {
|
||||||
|
return "", fmt.Errorf("AUTHORIZATION-APP-API has no user id")
|
||||||
|
}
|
||||||
|
return parts[0], nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package gonexapp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.neitzel.de/konrad/go-nc-exapp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUserFromRequestRoundTrip(t *testing.T) {
|
||||||
|
token := base64.StdEncoding.EncodeToString([]byte("alice:secret"))
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
r.Header.Set("AUTHORIZATION-APP-API", token)
|
||||||
|
user, err := gonexapp.UserFromRequest(r)
|
||||||
|
if err != nil || user != "alice" {
|
||||||
|
t.Fatalf("user=%q err=%v", user, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserFromRequestMissing(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
_, err := gonexapp.UserFromRequest(r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for missing header")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserFromRequestInvalidBase64(t *testing.T) {
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
r.Header.Set("AUTHORIZATION-APP-API", "%%%")
|
||||||
|
_, err := gonexapp.UserFromRequest(r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for invalid base64")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUserFromRequestNoUserID(t *testing.T) {
|
||||||
|
token := base64.StdEncoding.EncodeToString([]byte(":secret"))
|
||||||
|
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
|
r.Header.Set("AUTHORIZATION-APP-API", token)
|
||||||
|
_, err := gonexapp.UserFromRequest(r)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for empty user id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithUser(t *testing.T) {
|
||||||
|
cred := gonexapp.Credentials{UserID: "alice"}
|
||||||
|
asBob := cred.WithUser("bob")
|
||||||
|
if asBob.UserID != "bob" {
|
||||||
|
t.Fatalf("user=%q", asBob.UserID)
|
||||||
|
}
|
||||||
|
if cred.UserID != "alice" {
|
||||||
|
t.Fatalf("original mutated: %q", cred.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAuthHeaders(t *testing.T) {
|
||||||
|
cred := gonexapp.Credentials{
|
||||||
|
BaseURL: "https://nc.example",
|
||||||
|
AppID: "myapp",
|
||||||
|
AppVersion: "1.0.0",
|
||||||
|
AAVersion: "2.0.0",
|
||||||
|
AppSecret: "secret",
|
||||||
|
UserID: "alice",
|
||||||
|
}
|
||||||
|
h := cred.AuthHeaders()
|
||||||
|
if got := h.Get("EX-APP-ID"); got != "myapp" {
|
||||||
|
t.Fatalf("EX-APP-ID=%q", got)
|
||||||
|
}
|
||||||
|
if got := h.Get("AA-VERSION"); got != "2.0.0" {
|
||||||
|
t.Fatalf("AA-VERSION=%q", got)
|
||||||
|
}
|
||||||
|
wantToken := base64.StdEncoding.EncodeToString([]byte("alice:secret"))
|
||||||
|
if got := h.Get("AUTHORIZATION-APP-API"); got != wantToken {
|
||||||
|
t.Fatalf("AUTHORIZATION-APP-API=%q want %q", got, wantToken)
|
||||||
|
}
|
||||||
|
if got := h.Get("OCS-APIRequest"); got != "true" {
|
||||||
|
t.Fatalf("OCS-APIRequest=%q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// Package gonexapp provides AppAPI credentials, OCS JSON calls, and ExApp user
|
||||||
|
// preferences for Nextcloud ExApp Services.
|
||||||
|
package gonexapp
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package gonexapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OCSClient performs AppAPI-authenticated OCS requests with format=json.
|
||||||
|
type OCSClient struct {
|
||||||
|
Cred Credentials
|
||||||
|
Client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c OCSClient) httpClient() *http.Client {
|
||||||
|
if c.Client != nil {
|
||||||
|
return c.Client
|
||||||
|
}
|
||||||
|
return http.DefaultClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL builds an OCS URL under /ocs/v2.php with format=json.
|
||||||
|
func (c OCSClient) URL(path string) string {
|
||||||
|
base := strings.TrimRight(c.Cred.BaseURL, "/")
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
return base + "/ocs/v2.php/" + path + "?format=json"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call performs method against path with an optional JSON body and returns the response body
|
||||||
|
// after verifying it is valid JSON. Non-2xx responses return an error including the status.
|
||||||
|
func (c OCSClient) Call(method, path string, body []byte) ([]byte, error) {
|
||||||
|
var reader io.Reader
|
||||||
|
if len(body) > 0 {
|
||||||
|
reader = bytes.NewReader(body)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, c.URL(path), reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(body) > 0 {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
for k, vs := range c.Cred.AuthHeaders() {
|
||||||
|
for _, v := range vs {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res, err := c.httpClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
raw, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("ocs %s %s: %s", method, path, res.Status)
|
||||||
|
}
|
||||||
|
if !json.Valid(raw) {
|
||||||
|
return nil, fmt.Errorf("ocs %s %s: response is not JSON", method, path)
|
||||||
|
}
|
||||||
|
return raw, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package gonexapp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppAPIPreferences reads and writes one string ExApp preference for the requesting user.
|
||||||
|
type AppAPIPreferences struct {
|
||||||
|
Cred Credentials
|
||||||
|
AppID string
|
||||||
|
Key string
|
||||||
|
Client *http.Client
|
||||||
|
OCS OCSClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppAPIPreferences returns a preference store for appID and key using cred for auth.
|
||||||
|
func NewAppAPIPreferences(cred Credentials, appID, key string) AppAPIPreferences {
|
||||||
|
return AppAPIPreferences{
|
||||||
|
Cred: cred,
|
||||||
|
AppID: appID,
|
||||||
|
Key: key,
|
||||||
|
OCS: OCSClient{Cred: cred},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p AppAPIPreferences) ocsClient() OCSClient {
|
||||||
|
c := p.OCS
|
||||||
|
if c.Cred.BaseURL == "" {
|
||||||
|
c.Cred = p.Cred
|
||||||
|
}
|
||||||
|
if c.Client == nil {
|
||||||
|
c.Client = p.Client
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get loads the configured preference key for the requesting user.
|
||||||
|
func (p AppAPIPreferences) Get() (string, error) {
|
||||||
|
ocs := p.ocsClient()
|
||||||
|
body, _ := json.Marshal(map[string]any{"configKeys": []string{p.Key}})
|
||||||
|
raw, err := ocs.Call(http.MethodPost, "apps/app_api/api/v1/ex-app/preference/get-values", body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return decodePreferenceValue(raw, p.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set stores value for the configured preference key.
|
||||||
|
func (p AppAPIPreferences) Set(value string) error {
|
||||||
|
ocs := p.ocsClient()
|
||||||
|
body, _ := json.Marshal(map[string]any{
|
||||||
|
"configKey": p.Key,
|
||||||
|
"configValue": value,
|
||||||
|
"sensitive": 0,
|
||||||
|
})
|
||||||
|
_, err := ocs.Call(http.MethodPost, "apps/app_api/api/v1/ex-app/preference", body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePreferenceValue(raw []byte, key string) (string, error) {
|
||||||
|
var parsed struct {
|
||||||
|
OCS struct {
|
||||||
|
Data []struct {
|
||||||
|
ConfigKey string `json:"configkey"`
|
||||||
|
ConfigValue string `json:"configvalue"`
|
||||||
|
} `json:"data"`
|
||||||
|
} `json:"ocs"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||||
|
var items []struct {
|
||||||
|
ConfigKey string `json:"configkey"`
|
||||||
|
ConfigValue string `json:"configvalue"`
|
||||||
|
}
|
||||||
|
if err2 := json.Unmarshal(raw, &items); err2 != nil {
|
||||||
|
return "", fmt.Errorf("preferences decode: %w", err)
|
||||||
|
}
|
||||||
|
for _, it := range items {
|
||||||
|
if it.ConfigKey == key {
|
||||||
|
return strings.TrimSpace(it.ConfigValue), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
for _, it := range parsed.OCS.Data {
|
||||||
|
if it.ConfigKey == key {
|
||||||
|
return strings.TrimSpace(it.ConfigValue), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package gonexapp_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.neitzel.de/konrad/go-nc-exapp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOCSCallIncludesFormatJSON(t *testing.T) {
|
||||||
|
var gotPath string
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.String()
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"ok": "true"})
|
||||||
|
}))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
cred := gonexapp.Credentials{
|
||||||
|
BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice",
|
||||||
|
}
|
||||||
|
client := gonexapp.OCSClient{Cred: cred, Client: srv.Client()}
|
||||||
|
raw, err := client.Call(http.MethodGet, "apps/app_api/api/v1/ex-app/preference/get-values", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !json.Valid(raw) {
|
||||||
|
t.Fatalf("not json: %s", raw)
|
||||||
|
}
|
||||||
|
if !strings.Contains(gotPath, "format=json") {
|
||||||
|
t.Fatalf("path missing format=json: %s", gotPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOCSCallRejectsNonJSON(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = io.WriteString(w, "<xml/>")
|
||||||
|
}))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
cred := gonexapp.Credentials{BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice"}
|
||||||
|
client := gonexapp.OCSClient{Cred: cred, Client: srv.Client()}
|
||||||
|
_, err := client.Call(http.MethodGet, "apps/example", nil)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "not JSON") {
|
||||||
|
t.Fatalf("got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOCSCallSurfacesHTTPError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Error(w, "nope", http.StatusBadRequest)
|
||||||
|
}))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
cred := gonexapp.Credentials{BaseURL: srv.URL, AppID: "app", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice"}
|
||||||
|
client := gonexapp.OCSClient{Cred: cred, Client: srv.Client()}
|
||||||
|
_, err := client.Call(http.MethodPost, "apps/example", []byte(`{}`))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "400") {
|
||||||
|
t.Fatalf("got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppAPIPreferencesGetSet(t *testing.T) {
|
||||||
|
const prefKey = "savedDefault"
|
||||||
|
prefsVals := map[string]string{}
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch {
|
||||||
|
case strings.Contains(r.URL.Path, "ex-app/preference/get-values"):
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"ocs": map[string]any{
|
||||||
|
"data": []map[string]string{{
|
||||||
|
"configkey": prefKey,
|
||||||
|
"configvalue": prefsVals[prefKey],
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
case strings.HasSuffix(r.URL.Path, "ex-app/preference") && r.Method == http.MethodPost:
|
||||||
|
var body struct {
|
||||||
|
ConfigKey string `json:"configKey"`
|
||||||
|
ConfigValue string `json:"configValue"`
|
||||||
|
}
|
||||||
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
prefsVals[body.ConfigKey] = body.ConfigValue
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
default:
|
||||||
|
http.Error(w, "unhandled "+r.Method+" "+r.URL.Path, http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
|
||||||
|
cred := gonexapp.Credentials{
|
||||||
|
BaseURL: srv.URL, AppID: "myexapp", AppVersion: "0.1.0", AAVersion: "1.0.0", AppSecret: "s", UserID: "alice",
|
||||||
|
}
|
||||||
|
store := gonexapp.NewAppAPIPreferences(cred, "myexapp", prefKey)
|
||||||
|
store.Client = srv.Client()
|
||||||
|
store.OCS.Client = srv.Client()
|
||||||
|
|
||||||
|
got, err := store.Get()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != "" {
|
||||||
|
t.Fatalf("initial value=%q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := store.Set("Zones"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if prefsVals[prefKey] != "Zones" {
|
||||||
|
t.Fatalf("prefs not written: %#v", prefsVals)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err = store.Get()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != "Zones" {
|
||||||
|
t.Fatalf("got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user