Nik Afiq 8f7024edfa
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
feat(climate): add SetTemperature method to ClimateService
- 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.
2026-07-25 12:08:24 +09:00

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")
}