All checks were successful
CI / changes (push) Successful in 19s
CI / test (push) Successful in 24s
CI / build-ai-gateway (push) Successful in 1m6s
CI / build-ha-gateway (push) Successful in 1m3s
CI / build-discord-bot (push) Successful in 1m4s
CI / build-alexa-bridge (push) Successful in 1m15s
CI / build-tts-gateway (push) Successful in 1m5s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
- Implemented SetTemperature in ClimateService for setting an absolute target temperature. - Updated ClimateServiceClient and ClimateServiceServer interfaces to include SetTemperature. - Added corresponding handler and tests for SetTemperature in ClimateGRPC. - Modified ClimateApp to handle SetTemperature requests without clamping. - Updated climate.proto to define SetTemperatureRequest message. - Adjusted Dockerfiles to include alexa-bridge dependencies.
178 lines
5.3 KiB
Go
178 lines
5.3 KiB
Go
package alexa
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type fakeValidator struct {
|
|
err error
|
|
}
|
|
|
|
func (f *fakeValidator) Validate(ctx context.Context, r *http.Request, body []byte) error {
|
|
return f.err
|
|
}
|
|
|
|
type fakeDispatcher struct {
|
|
respFunc func(ctx context.Context, req Request) (Response, error)
|
|
}
|
|
|
|
func (f *fakeDispatcher) Dispatch(ctx context.Context, req Request) (Response, error) {
|
|
if f.respFunc == nil {
|
|
return NewTellResponse("ok"), nil
|
|
}
|
|
return f.respFunc(ctx, req)
|
|
}
|
|
|
|
const testSkillID = "amzn1.ask.skill.test"
|
|
|
|
func validRequestBody(t *testing.T, skillID, requestType string, ts time.Time) []byte {
|
|
t.Helper()
|
|
req := Request{
|
|
Version: "1.0",
|
|
Context: &Context{System: System{Application: Application{ApplicationID: skillID}}},
|
|
Request: RequestBody{
|
|
Type: requestType,
|
|
RequestID: "amzn1.echo-api.request.test",
|
|
Timestamp: ts.UTC().Format(time.RFC3339),
|
|
Locale: "en-US",
|
|
},
|
|
}
|
|
b, err := json.Marshal(req)
|
|
if err != nil {
|
|
t.Fatalf("marshal request: %v", err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestHandlerServeHTTP(t *testing.T) {
|
|
t.Run("happy path returns the dispatcher's response", func(t *testing.T) {
|
|
body := validRequestBody(t, testSkillID, "LaunchRequest", time.Now())
|
|
h := NewHandler(&fakeValidator{}, testSkillID, &fakeDispatcher{
|
|
respFunc: func(ctx context.Context, req Request) (Response, error) {
|
|
return NewTellResponse("hello"), nil
|
|
},
|
|
})
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d (body=%s)", rec.Code, http.StatusOK, rec.Body.String())
|
|
}
|
|
var got Response
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
|
|
t.Fatalf("unmarshal response: %v", err)
|
|
}
|
|
if got.Response.OutputSpeech.Text != "hello" {
|
|
t.Fatalf("OutputSpeech.Text = %q, want %q", got.Response.OutputSpeech.Text, "hello")
|
|
}
|
|
if ct := rec.Header().Get("Content-Type"); ct != "application/json" {
|
|
t.Fatalf("Content-Type = %q, want application/json", ct)
|
|
}
|
|
})
|
|
|
|
t.Run("signature validation failure returns 401 without dispatching", func(t *testing.T) {
|
|
dispatched := false
|
|
body := validRequestBody(t, testSkillID, "LaunchRequest", time.Now())
|
|
h := NewHandler(&fakeValidator{err: errors.New("bad signature")}, testSkillID, &fakeDispatcher{
|
|
respFunc: func(ctx context.Context, req Request) (Response, error) {
|
|
dispatched = true
|
|
return NewTellResponse("ok"), nil
|
|
},
|
|
})
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
|
|
}
|
|
if dispatched {
|
|
t.Fatal("dispatcher should not be called when signature validation fails")
|
|
}
|
|
})
|
|
|
|
t.Run("malformed json body returns 400", func(t *testing.T) {
|
|
h := NewHandler(&fakeValidator{}, testSkillID, &fakeDispatcher{})
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("not json"))
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
|
}
|
|
})
|
|
|
|
t.Run("stale timestamp returns 401", func(t *testing.T) {
|
|
body := validRequestBody(t, testSkillID, "LaunchRequest", time.Now().Add(-10*time.Minute))
|
|
h := NewHandler(&fakeValidator{}, testSkillID, &fakeDispatcher{})
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
|
|
}
|
|
})
|
|
|
|
t.Run("application id mismatch returns 401", func(t *testing.T) {
|
|
body := validRequestBody(t, "some-other-skill", "LaunchRequest", time.Now())
|
|
h := NewHandler(&fakeValidator{}, testSkillID, &fakeDispatcher{})
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
|
|
}
|
|
})
|
|
|
|
t.Run("dispatcher error returns 500", func(t *testing.T) {
|
|
body := validRequestBody(t, testSkillID, "IntentRequest", time.Now())
|
|
h := NewHandler(&fakeValidator{}, testSkillID, &fakeDispatcher{
|
|
respFunc: func(ctx context.Context, req Request) (Response, error) {
|
|
return Response{}, errors.New("boom")
|
|
},
|
|
})
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(string(body)))
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
|
|
}
|
|
})
|
|
|
|
t.Run("body read error returns 400", func(t *testing.T) {
|
|
h := NewHandler(&fakeValidator{}, testSkillID, &fakeDispatcher{})
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/", io.NopCloser(&errReader{}))
|
|
h.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusBadRequest)
|
|
}
|
|
})
|
|
}
|
|
|
|
type errReader struct{}
|
|
|
|
func (e *errReader) Read(p []byte) (int, error) {
|
|
return 0, errors.New("read failed")
|
|
}
|