mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[PM-3883] Fix ConfigService.serverConfig$ initial values and error handling (#6272)
* Always fetch ServerConfig from server, use stored value as fallback * Handle errors in server fetch
This commit is contained in:
parent
a6e4ad4e7e
commit
931a2258e2
@ -540,6 +540,7 @@ export default class MainBackground {
|
|||||||
this.configApiService,
|
this.configApiService,
|
||||||
this.authService,
|
this.authService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
|
this.logService,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
this.browserPopoutWindowService = new BrowserPopoutWindowService();
|
this.browserPopoutWindowService = new BrowserPopoutWindowService();
|
||||||
|
@ -4,6 +4,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
|||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/services/config/config.service";
|
||||||
|
|
||||||
@ -19,8 +20,9 @@ export class BrowserConfigService extends ConfigService {
|
|||||||
configApiService: ConfigApiServiceAbstraction,
|
configApiService: ConfigApiServiceAbstraction,
|
||||||
authService: AuthService,
|
authService: AuthService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
|
logService: LogService,
|
||||||
subscribe = false
|
subscribe = false
|
||||||
) {
|
) {
|
||||||
super(stateService, configApiService, authService, environmentService, subscribe);
|
super(stateService, configApiService, authService, environmentService, logService, subscribe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,10 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
|
|||||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||||
import { FileUploadService } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
|
import { FileUploadService } from "@bitwarden/common/platform/abstractions/file-upload/file-upload.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service";
|
import {
|
||||||
|
LogService,
|
||||||
|
LogService as LogServiceAbstraction,
|
||||||
|
} from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import {
|
import {
|
||||||
@ -502,6 +505,7 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
ConfigApiServiceAbstraction,
|
ConfigApiServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
EnvironmentService,
|
EnvironmentService,
|
||||||
|
LogService,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -647,6 +647,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
ConfigApiServiceAbstraction,
|
ConfigApiServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentServiceAbstraction,
|
||||||
|
LogService,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@ import { AuthenticationStatus } from "../../../auth/enums/authentication-status"
|
|||||||
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction";
|
||||||
import { ServerConfig } from "../../abstractions/config/server-config";
|
import { ServerConfig } from "../../abstractions/config/server-config";
|
||||||
import { EnvironmentService } from "../../abstractions/environment.service";
|
import { EnvironmentService } from "../../abstractions/environment.service";
|
||||||
|
import { LogService } from "../../abstractions/log.service";
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../../abstractions/state.service";
|
||||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||||
import {
|
import {
|
||||||
@ -21,6 +22,7 @@ describe("ConfigService", () => {
|
|||||||
let configApiService: MockProxy<ConfigApiServiceAbstraction>;
|
let configApiService: MockProxy<ConfigApiServiceAbstraction>;
|
||||||
let authService: MockProxy<AuthService>;
|
let authService: MockProxy<AuthService>;
|
||||||
let environmentService: MockProxy<EnvironmentService>;
|
let environmentService: MockProxy<EnvironmentService>;
|
||||||
|
let logService: MockProxy<LogService>;
|
||||||
|
|
||||||
let serverResponseCount: number; // increments to track distinct responses received from server
|
let serverResponseCount: number; // increments to track distinct responses received from server
|
||||||
|
|
||||||
@ -31,7 +33,8 @@ describe("ConfigService", () => {
|
|||||||
stateService,
|
stateService,
|
||||||
configApiService,
|
configApiService,
|
||||||
authService,
|
authService,
|
||||||
environmentService
|
environmentService,
|
||||||
|
logService
|
||||||
);
|
);
|
||||||
configService.init();
|
configService.init();
|
||||||
return configService;
|
return configService;
|
||||||
@ -42,6 +45,8 @@ describe("ConfigService", () => {
|
|||||||
configApiService = mock();
|
configApiService = mock();
|
||||||
authService = mock();
|
authService = mock();
|
||||||
environmentService = mock();
|
environmentService = mock();
|
||||||
|
logService = mock();
|
||||||
|
|
||||||
environmentService.urls = new ReplaySubject<void>(1);
|
environmentService.urls = new ReplaySubject<void>(1);
|
||||||
|
|
||||||
serverResponseCount = 1;
|
serverResponseCount = 1;
|
||||||
@ -56,10 +61,12 @@ describe("ConfigService", () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Loads config from storage", (done) => {
|
it("Uses storage as fallback", (done) => {
|
||||||
const storedConfigData = serverConfigDataFactory("storedConfig");
|
const storedConfigData = serverConfigDataFactory("storedConfig");
|
||||||
stateService.getServerConfig.mockResolvedValueOnce(storedConfigData);
|
stateService.getServerConfig.mockResolvedValueOnce(storedConfigData);
|
||||||
|
|
||||||
|
configApiService.get.mockRejectedValueOnce(new Error("Unable to fetch"));
|
||||||
|
|
||||||
const configService = configServiceFactory();
|
const configService = configServiceFactory();
|
||||||
|
|
||||||
configService.serverConfig$.pipe(take(1)).subscribe((config) => {
|
configService.serverConfig$.pipe(take(1)).subscribe((config) => {
|
||||||
@ -68,6 +75,30 @@ describe("ConfigService", () => {
|
|||||||
expect(stateService.setServerConfig).not.toHaveBeenCalled();
|
expect(stateService.setServerConfig).not.toHaveBeenCalled();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
configService.triggerServerConfigFetch();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Stream does not error out if fetch fails", (done) => {
|
||||||
|
const storedConfigData = serverConfigDataFactory("storedConfig");
|
||||||
|
stateService.getServerConfig.mockResolvedValueOnce(storedConfigData);
|
||||||
|
|
||||||
|
const configService = configServiceFactory();
|
||||||
|
|
||||||
|
configService.serverConfig$.pipe(skip(1), take(1)).subscribe((config) => {
|
||||||
|
try {
|
||||||
|
expect(config.gitHash).toEqual("server1");
|
||||||
|
done();
|
||||||
|
} catch (e) {
|
||||||
|
done(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
configApiService.get.mockRejectedValueOnce(new Error("Unable to fetch"));
|
||||||
|
configService.triggerServerConfigFetch();
|
||||||
|
|
||||||
|
configApiService.get.mockResolvedValueOnce(serverConfigResponseFactory("server1"));
|
||||||
|
configService.triggerServerConfigFetch();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Fetches config from server", () => {
|
describe("Fetches config from server", () => {
|
||||||
@ -80,8 +111,8 @@ describe("ConfigService", () => {
|
|||||||
(hours: number, done: jest.DoneCallback) => {
|
(hours: number, done: jest.DoneCallback) => {
|
||||||
const configService = configServiceFactory();
|
const configService = configServiceFactory();
|
||||||
|
|
||||||
// skip initial load from storage, plus previous hours (if any)
|
// skip previous hours (if any)
|
||||||
configService.serverConfig$.pipe(skip(hours), take(1)).subscribe((config) => {
|
configService.serverConfig$.pipe(skip(hours - 1), take(1)).subscribe((config) => {
|
||||||
try {
|
try {
|
||||||
expect(config.gitHash).toEqual("server" + hours);
|
expect(config.gitHash).toEqual("server" + hours);
|
||||||
expect(configApiService.get).toHaveBeenCalledTimes(hours);
|
expect(configApiService.get).toHaveBeenCalledTimes(hours);
|
||||||
@ -99,8 +130,7 @@ describe("ConfigService", () => {
|
|||||||
it("when environment URLs change", (done) => {
|
it("when environment URLs change", (done) => {
|
||||||
const configService = configServiceFactory();
|
const configService = configServiceFactory();
|
||||||
|
|
||||||
// skip initial load from storage
|
configService.serverConfig$.pipe(take(1)).subscribe((config) => {
|
||||||
configService.serverConfig$.pipe(skip(1), take(1)).subscribe((config) => {
|
|
||||||
try {
|
try {
|
||||||
expect(config.gitHash).toEqual("server1");
|
expect(config.gitHash).toEqual("server1");
|
||||||
done();
|
done();
|
||||||
@ -115,8 +145,7 @@ describe("ConfigService", () => {
|
|||||||
it("when triggerServerConfigFetch() is called", (done) => {
|
it("when triggerServerConfigFetch() is called", (done) => {
|
||||||
const configService = configServiceFactory();
|
const configService = configServiceFactory();
|
||||||
|
|
||||||
// skip initial load from storage
|
configService.serverConfig$.pipe(take(1)).subscribe((config) => {
|
||||||
configService.serverConfig$.pipe(skip(1), take(1)).subscribe((config) => {
|
|
||||||
try {
|
try {
|
||||||
expect(config.gitHash).toEqual("server1");
|
expect(config.gitHash).toEqual("server1");
|
||||||
done();
|
done();
|
||||||
@ -134,8 +163,7 @@ describe("ConfigService", () => {
|
|||||||
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
|
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Locked);
|
||||||
const configService = configServiceFactory();
|
const configService = configServiceFactory();
|
||||||
|
|
||||||
// skip initial load from storage
|
configService.serverConfig$.pipe(take(1)).subscribe(() => {
|
||||||
configService.serverConfig$.pipe(skip(1), take(1)).subscribe(() => {
|
|
||||||
try {
|
try {
|
||||||
expect(stateService.setServerConfig).toHaveBeenCalledWith(
|
expect(stateService.setServerConfig).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({ gitHash: "server1" })
|
expect.objectContaining({ gitHash: "server1" })
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
ReplaySubject,
|
ReplaySubject,
|
||||||
Subject,
|
Subject,
|
||||||
|
catchError,
|
||||||
concatMap,
|
concatMap,
|
||||||
|
defer,
|
||||||
delayWhen,
|
delayWhen,
|
||||||
filter,
|
|
||||||
firstValueFrom,
|
firstValueFrom,
|
||||||
from,
|
|
||||||
map,
|
map,
|
||||||
merge,
|
merge,
|
||||||
timer,
|
timer,
|
||||||
@ -18,6 +18,7 @@ import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-ap
|
|||||||
import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction";
|
import { ConfigServiceAbstraction } from "../../abstractions/config/config.service.abstraction";
|
||||||
import { ServerConfig } from "../../abstractions/config/server-config";
|
import { ServerConfig } from "../../abstractions/config/server-config";
|
||||||
import { EnvironmentService, Region } from "../../abstractions/environment.service";
|
import { EnvironmentService, Region } from "../../abstractions/environment.service";
|
||||||
|
import { LogService } from "../../abstractions/log.service";
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../../abstractions/state.service";
|
||||||
import { ServerConfigData } from "../../models/data/server-config.data";
|
import { ServerConfigData } from "../../models/data/server-config.data";
|
||||||
|
|
||||||
@ -38,6 +39,7 @@ export class ConfigService implements ConfigServiceAbstraction {
|
|||||||
private configApiService: ConfigApiServiceAbstraction,
|
private configApiService: ConfigApiServiceAbstraction,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
|
private logService: LogService,
|
||||||
|
|
||||||
// Used to avoid duplicate subscriptions, e.g. in browser between the background and popup
|
// Used to avoid duplicate subscriptions, e.g. in browser between the background and popup
|
||||||
private subscribe = true
|
private subscribe = true
|
||||||
@ -48,14 +50,16 @@ export class ConfigService implements ConfigServiceAbstraction {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get config from storage on initial load
|
const latestServerConfig$ = defer(() => this.configApiService.get()).pipe(
|
||||||
const fromStorage = from(this.stateService.getServerConfig()).pipe(
|
map((response) => new ServerConfigData(response)),
|
||||||
map((data) => (data == null ? null : new ServerConfig(data)))
|
delayWhen((data) => this.saveConfig(data)),
|
||||||
|
catchError((e: unknown) => {
|
||||||
|
// fall back to stored ServerConfig (if any)
|
||||||
|
this.logService.error("Unable to fetch ServerConfig: " + (e as Error)?.message);
|
||||||
|
return this.stateService.getServerConfig();
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
fromStorage.subscribe((config) => this._serverConfig.next(config));
|
|
||||||
|
|
||||||
// Fetch config from server
|
|
||||||
// If you need to fetch a new config when an event occurs, add an observable that emits on that event here
|
// If you need to fetch a new config when an event occurs, add an observable that emits on that event here
|
||||||
merge(
|
merge(
|
||||||
timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS), // after 1 hour, then every hour
|
timer(ONE_HOUR_IN_MILLISECONDS, ONE_HOUR_IN_MILLISECONDS), // after 1 hour, then every hour
|
||||||
@ -63,12 +67,8 @@ export class ConfigService implements ConfigServiceAbstraction {
|
|||||||
this._forceFetchConfig // manual
|
this._forceFetchConfig // manual
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
delayWhen(() => fromStorage), // wait until storage has emitted first to avoid a race condition
|
concatMap(() => latestServerConfig$),
|
||||||
concatMap(() => this.configApiService.get()),
|
map((data) => (data == null ? null : new ServerConfig(data)))
|
||||||
filter((response) => response != null),
|
|
||||||
map((response) => new ServerConfigData(response)),
|
|
||||||
delayWhen((data) => this.saveConfig(data)),
|
|
||||||
map((data) => new ServerConfig(data))
|
|
||||||
)
|
)
|
||||||
.subscribe((config) => this._serverConfig.next(config));
|
.subscribe((config) => this._serverConfig.next(config));
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user