build: add test-all make targets

This commit is contained in:
Haitao Pan 2026-04-08 16:37:12 +08:00
parent 319d7a383f
commit ea4abe350b
13 changed files with 232 additions and 12 deletions

View File

@ -16,7 +16,7 @@ APP_BUILD_NUMBER := $(if $(APP_BUILD_NUMBER_RAW),$(APP_BUILD_NUMBER_RAW),1)
APP_DART_DEFINE_VERSION ?= --dart-define=XWORKMATE_DISPLAY_VERSION=$(APP_VERSION)
APP_DART_DEFINE_BUILD ?= --dart-define=XWORKMATE_BUILD_NUMBER=$(APP_BUILD_NUMBER)
.PHONY: help deps analyze test check format run open-macos-xcode sync-version build-linux build-macos build-ios-sim package-deb package-rpm package-linux package-mac install-mac clean build-go-core render-release-docs check-export-compliance
.PHONY: help deps analyze test test-all test-flutter test-golden test-integration test-integration-macos test-patrol test-go test-ci check format run open-macos-xcode sync-version build-linux build-macos build-ios-sim package-deb package-rpm package-linux package-mac install-mac clean build-go-core render-release-docs check-export-compliance
help: ## Show available targets
@grep -E '^[a-zA-Z0-9_.-]+:.*?## ' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-18s %s\n", $$1, $$2}'
@ -30,6 +30,30 @@ analyze: ## Run static analysis
test: ## Run Flutter tests
$(FLUTTER) test
test-flutter: ## Run the full Flutter unit/widget test suite
$(FLUTTER) test
test-golden: ## Run Flutter Golden tests
$(FLUTTER) test test/golden
test-integration: ## Run Flutter integration tests
$(FLUTTER) test integration_test
test-integration-macos: ## Run macOS integration tests serially for the desktop app
$(FLUTTER) test integration_test/desktop_navigation_flow_test.dart -d macos
$(FLUTTER) test integration_test/desktop_settings_flow_test.dart -d macos
test-patrol: ## Run Patrol end-to-end tests
dart pub global activate patrol_cli
patrol test
test-go: ## Run Go API unit tests
cd go_service && go test ./...
test-ci: test-flutter test-golden test-integration test-go ## Run the PR validation chain
test-all: test-ci test-patrol ## Run the full local validation chain
check: analyze test ## Run the standard validation suite
format: ## Format Dart sources

View File

@ -0,0 +1,49 @@
package handler
import (
"encoding/json"
"net/http"
"xworkmate/go_core/internal/service"
)
type Authenticator interface {
Authenticate(username, password string) error
}
type AuthHandler struct {
service Authenticator
}
func NewAuthHandler(svc Authenticator) *AuthHandler {
return &AuthHandler{service: svc}
}
func NewServiceAdapter(svc *service.AuthService) Authenticator {
return authServiceAdapter{service: svc}
}
func (h *AuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var payload struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if err := h.service.Authenticate(payload.Username, payload.Password); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
type authServiceAdapter struct {
service *service.AuthService
}
func (a authServiceAdapter) Authenticate(username, password string) error {
return a.service.Authenticate(nil, username, password)
}

View File

@ -0,0 +1,53 @@
package handler
import (
"bytes"
"errors"
"net/http"
"net/http/httptest"
"testing"
)
type fakeAuthenticator struct {
err error
}
func (f fakeAuthenticator) Authenticate(username, password string) error {
return f.err
}
func TestAuthHandlerRejectsInvalidJSON(t *testing.T) {
handler := NewAuthHandler(fakeAuthenticator{})
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString("{"))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
}
func TestAuthHandlerReturnsUnauthorizedOnServiceFailure(t *testing.T) {
handler := NewAuthHandler(fakeAuthenticator{err: errors.New("invalid credentials")})
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString(`{"username":"alice","password":"secret"}`))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
}
func TestAuthHandlerReturnsOKOnSuccess(t *testing.T) {
handler := NewAuthHandler(fakeAuthenticator{})
req := httptest.NewRequest(http.MethodPost, "/auth", bytes.NewBufferString(`{"username":"alice","password":"secret"}`))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rec.Code)
}
}

View File

@ -0,0 +1,37 @@
package service
import (
"context"
"errors"
"strings"
)
var ErrInvalidCredentials = errors.New("invalid credentials")
type AuthRepository interface {
Verify(ctx context.Context, username, password string) (bool, error)
}
type AuthService struct {
repo AuthRepository
}
func NewAuthService(repo AuthRepository) *AuthService {
return &AuthService{repo: repo}
}
func (s *AuthService) Authenticate(ctx context.Context, username, password string) error {
username = strings.TrimSpace(username)
password = strings.TrimSpace(password)
if username == "" || password == "" {
return ErrInvalidCredentials
}
ok, err := s.repo.Verify(ctx, username, password)
if err != nil {
return err
}
if !ok {
return ErrInvalidCredentials
}
return nil
}

View File

@ -0,0 +1,55 @@
package service
import (
"context"
"errors"
"testing"
)
type fakeAuthRepo struct {
verify func(ctx context.Context, username, password string) (bool, error)
}
func (f fakeAuthRepo) Verify(ctx context.Context, username, password string) (bool, error) {
return f.verify(ctx, username, password)
}
func TestAuthenticateRejectsBlankValues(t *testing.T) {
svc := NewAuthService(fakeAuthRepo{
verify: func(ctx context.Context, username, password string) (bool, error) {
return true, nil
},
})
if err := svc.Authenticate(context.Background(), " ", "secret"); !errors.Is(err, ErrInvalidCredentials) {
t.Fatalf("expected invalid credentials, got %v", err)
}
}
func TestAuthenticateRejectsFailedVerification(t *testing.T) {
svc := NewAuthService(fakeAuthRepo{
verify: func(ctx context.Context, username, password string) (bool, error) {
if username != "alice" || password != "secret" {
t.Fatalf("unexpected credentials: %q %q", username, password)
}
return false, nil
},
})
if err := svc.Authenticate(context.Background(), "alice", "secret"); !errors.Is(err, ErrInvalidCredentials) {
t.Fatalf("expected invalid credentials, got %v", err)
}
}
func TestAuthenticateReturnsRepoError(t *testing.T) {
wanted := errors.New("boom")
svc := NewAuthService(fakeAuthRepo{
verify: func(ctx context.Context, username, password string) (bool, error) {
return false, wanted
},
})
if err := svc.Authenticate(context.Background(), "alice", "secret"); !errors.Is(err, wanted) {
t.Fatalf("expected repo error, got %v", err)
}
}

View File

@ -84,6 +84,7 @@ class _XWorkmateAppState extends State<XWorkmateApp> {
animation: _controller,
builder: (context, _) {
return MaterialApp(
key: const Key('xworkmate-app-shell'),
title: kSystemAppName,
debugShowCheckedModeBanner: false,
locale: Locale(_controller.appLanguage.code),

View File

@ -628,6 +628,7 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
),
},
child: TextField(
key: const Key('assistant-input-field'),
controller: widget.inputController,
focusNode: widget.focusNode,
autofocus: true,
@ -789,8 +790,8 @@ class ComposerBarStateInternal extends State<ComposerBarInternal> {
const SizedBox(width: 8),
Tooltip(
message: submitLabel,
child: FilledButton(
key: const Key('assistant-submit-button'),
child: FilledButton(
key: const Key('assistant-send-button'),
onPressed: connecting
? null
: connected

View File

@ -227,7 +227,7 @@ extension SettingsPageGatewayAcpMixinInternal on SettingsPageStateInternal {
),
),
FilledButton(
key: ValueKey('external-acp-apply-${profile.providerKey}'),
key: ValueKey('external-acp-save-${profile.providerKey}'),
onPressed: () => saveExternalAcpEndpointInternal(
controller,
settings,

View File

@ -64,7 +64,7 @@ SPEC CHECKSUMS:
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
mobile_scanner: 0e365ed56cad24f28c0fd858ca04edefb40dfac3
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
patrol: 5df5d241d7f95f0df12a6906bbf45acb43a1e537
patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
super_native_extensions: c2795d6d9aedf4a79fae25cb6160b71b50549189

View File

@ -543,26 +543,26 @@ packages:
dependency: "direct dev"
description:
name: patrol
sha256: "32fd0709f3871fa56eb9cd88410e3ca816bfa757122bae806a0f842188acb820"
sha256: "7825a6e96a8f0755f68eec600a91a08b19bd0975488a70885b3696f6b65ffc0f"
url: "https://pub.dev"
source: hosted
version: "3.20.0"
version: "4.5.0"
patrol_finders:
dependency: transitive
description:
name: patrol_finders
sha256: "4a658d7d560de523f92deb3fa3326c78747ca0bf7e7f4b8788c012463138b628"
sha256: "9970eac0669a90b20ec7e1bcaabd0475655655998068ca656f4df9f6ec84f336"
url: "https://pub.dev"
source: hosted
version: "2.9.0"
version: "3.2.0"
patrol_log:
dependency: transitive
description:
name: patrol_log
sha256: "9fed4143980df1e3bbcfa00d0b443c7d68f04f9132317b7698bbc37f8a5a58c5"
sha256: a2360db165c34692665c0de146e5157887d6b584fdccca8f141f947a5acf1b2e
url: "https://pub.dev"
source: hosted
version: "0.5.0"
version: "0.8.0"
pixel_snap:
dependency: transitive
description:

View File

@ -38,7 +38,7 @@ dev_dependencies:
integration_test:
sdk: flutter
golden_toolkit: ^0.15.0
patrol: ^3.13.0
patrol: ^4.3.0
flutter_lints: ^6.0.0
dependency_overrides:

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB