mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-03-04 17:01:16 -05:00
feat(client): re-enable failed update re-submission (#46064)
* refactor: return response with data from ajax
Because we still need to manipulate the data coming back from the server
(files -> challengeFiles) and we want to keep ajax.ts as the interface
between client and server we need to return the manipulated data with
the response.
* feat: re-enable failed updates flushing
* test: failed updates get resubmitted and flushed
* fix: convert settings requests to use { data }
* refactor: use preserveSession
This commit is contained in:
committed by
GitHub
parent
fcf2dd7254
commit
f0f44ca315
@@ -8,10 +8,10 @@ import { acceptTermsComplete, acceptTermsError } from './';
|
||||
|
||||
function* acceptTermsSaga({ payload: quincyEmails }) {
|
||||
try {
|
||||
const response = yield call(putUserAcceptsTerms, quincyEmails);
|
||||
const { data } = yield call(putUserAcceptsTerms, quincyEmails);
|
||||
|
||||
yield put(acceptTermsComplete(quincyEmails));
|
||||
yield put(createFlashMessage(response));
|
||||
yield put(createFlashMessage(data));
|
||||
} catch (e) {
|
||||
yield put(acceptTermsError(e));
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ function* tryToShowCodeAllySaga() {
|
||||
yield put(showCodeAlly());
|
||||
} else {
|
||||
try {
|
||||
const response = yield call(postUserToken);
|
||||
const { data } = yield call(postUserToken);
|
||||
|
||||
if (response?.userToken) {
|
||||
yield put(updateUserToken(response.userToken));
|
||||
if (data?.userToken) {
|
||||
yield put(updateUserToken(data.userToken));
|
||||
yield put(showCodeAlly());
|
||||
} else {
|
||||
yield put(createFlashMessage(startProjectErrMessage));
|
||||
|
||||
@@ -103,7 +103,9 @@ function* postChargeStripeCardSaga({
|
||||
}) {
|
||||
try {
|
||||
const optimizedPayload = { paymentMethodId, amount, duration };
|
||||
const { error } = yield call(postChargeStripeCard, optimizedPayload);
|
||||
const {
|
||||
data: { error }
|
||||
} = yield call(postChargeStripeCard, optimizedPayload);
|
||||
if (error) {
|
||||
yield stripeCardErrorHandler(
|
||||
error,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import { ofType } from 'redux-observable';
|
||||
import { merge, empty } from 'rxjs';
|
||||
import {
|
||||
@@ -51,15 +50,14 @@ function failedUpdateEpic(action$, state$) {
|
||||
filter(() => store.get(key)),
|
||||
filter(() => isServerOnlineSelector(state$.value)),
|
||||
tap(() => {
|
||||
let failures = store.get(key) || [];
|
||||
let failures = store.get(key);
|
||||
failures = Array.isArray(failures) ? failures : [];
|
||||
|
||||
let submitableFailures = failures.filter(isSubmitable);
|
||||
|
||||
// delete unsubmitable failed challenges
|
||||
if (submitableFailures.length !== failures.length) {
|
||||
store.set(key, submitableFailures);
|
||||
failures = submitableFailures;
|
||||
}
|
||||
store.set(key, submitableFailures);
|
||||
failures = submitableFailures;
|
||||
|
||||
let delayTime = 100;
|
||||
const batch = failures.map((update, i) => {
|
||||
@@ -77,11 +75,8 @@ function failedUpdateEpic(action$, state$) {
|
||||
return delay(delayTime, () =>
|
||||
postUpdate$(update)
|
||||
.pipe(
|
||||
switchMap(response => {
|
||||
if (
|
||||
response &&
|
||||
(response.message || isGoodXHRStatus(response.status))
|
||||
) {
|
||||
switchMap(({ response, data }) => {
|
||||
if (data?.message || isGoodXHRStatus(response?.status)) {
|
||||
console.info(`${update.id} succeeded`);
|
||||
// the request completed successfully
|
||||
const failures = store.get(key) || [];
|
||||
@@ -104,8 +99,7 @@ function failedUpdateEpic(action$, state$) {
|
||||
ignoreElements()
|
||||
);
|
||||
|
||||
return storeUpdates;
|
||||
// return merge(storeUpdates, flushUpdates);
|
||||
return merge(storeUpdates, flushUpdates);
|
||||
}
|
||||
|
||||
export default failedUpdateEpic;
|
||||
|
||||
@@ -8,8 +8,7 @@ jest.mock('../analytics');
|
||||
|
||||
const key = 'fcc-failed-updates';
|
||||
|
||||
// TODO: re-enable once we start flushing failed updates again
|
||||
describe.skip('failed-updates-epic', () => {
|
||||
describe('failed-updates-epic', () => {
|
||||
it('should remove faulty backend challenges from localStorage', async () => {
|
||||
store.set(key, failedSubmissions);
|
||||
|
||||
|
||||
@@ -16,15 +16,14 @@ function* fetchSessionUser() {
|
||||
}
|
||||
try {
|
||||
const {
|
||||
user = {},
|
||||
result = '',
|
||||
sessionMeta = {}
|
||||
data: { user = {}, result = '', sessionMeta = {} }
|
||||
} = yield call(getSessionUser);
|
||||
const appUser = user[result] || {};
|
||||
yield put(
|
||||
fetchUserComplete({ user: appUser, username: result, sessionMeta })
|
||||
);
|
||||
} catch (e) {
|
||||
console.log('failed to fetch user', e);
|
||||
yield put(fetchUserError(e));
|
||||
}
|
||||
}
|
||||
@@ -33,10 +32,9 @@ function* fetchOtherUser({ payload: maybeUser = '' }) {
|
||||
try {
|
||||
const maybeUserLC = maybeUser.toLowerCase();
|
||||
|
||||
const { entities: { user = {} } = {}, result = '' } = yield call(
|
||||
getUserProfile,
|
||||
maybeUserLC
|
||||
);
|
||||
const {
|
||||
data: { entities: { user = {} } = {}, result = '' }
|
||||
} = yield call(getUserProfile, maybeUserLC);
|
||||
const otherUser = user[result] || {};
|
||||
yield put(
|
||||
fetchProfileForUserComplete({ user: otherUser, username: result })
|
||||
|
||||
@@ -8,10 +8,10 @@ import { reportUserComplete, reportUserError } from './';
|
||||
|
||||
function* reportUserSaga({ payload }) {
|
||||
try {
|
||||
const response = yield call(postReportUser, payload);
|
||||
const { data } = yield call(postReportUser, payload);
|
||||
|
||||
yield put(reportUserComplete());
|
||||
yield put(createFlashMessage(response));
|
||||
yield put(createFlashMessage(data));
|
||||
} catch (e) {
|
||||
yield put(reportUserError(e));
|
||||
}
|
||||
|
||||
@@ -46,14 +46,14 @@ export function* saveChallengeSaga() {
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
const response = yield call(postSaveChallenge, body);
|
||||
const { data } = yield call(postSaveChallenge, body);
|
||||
|
||||
if (response?.message) {
|
||||
yield put(createFlashMessage(response));
|
||||
} else if (response?.savedChallenges) {
|
||||
if (data?.message) {
|
||||
yield put(createFlashMessage(data));
|
||||
} else if (data?.savedChallenges) {
|
||||
yield put(
|
||||
saveChallengeComplete(
|
||||
mapFilesToChallengeFiles(response.savedChallenges)
|
||||
mapFilesToChallengeFiles(data.savedChallenges)
|
||||
)
|
||||
);
|
||||
yield put(
|
||||
|
||||
@@ -55,9 +55,9 @@ import {
|
||||
|
||||
function* submitNewAboutSaga({ payload }) {
|
||||
try {
|
||||
const response = yield call(putUpdateMyAbout, payload);
|
||||
yield put(submitNewAboutComplete({ ...response, payload }));
|
||||
yield put(createFlashMessage(response));
|
||||
const { data } = yield call(putUpdateMyAbout, payload);
|
||||
yield put(submitNewAboutComplete({ ...data, payload }));
|
||||
yield put(createFlashMessage(data));
|
||||
} catch (e) {
|
||||
yield put(submitNewAboutError(e));
|
||||
}
|
||||
@@ -65,9 +65,9 @@ function* submitNewAboutSaga({ payload }) {
|
||||
|
||||
function* submitNewUsernameSaga({ payload: username }) {
|
||||
try {
|
||||
const response = yield call(putUpdateMyUsername, username);
|
||||
yield put(submitNewUsernameComplete({ ...response, username }));
|
||||
yield put(createFlashMessage(response));
|
||||
const { data } = yield call(putUpdateMyUsername, username);
|
||||
yield put(submitNewUsernameComplete({ ...data, username }));
|
||||
yield put(createFlashMessage(data));
|
||||
} catch (e) {
|
||||
yield put(submitNewUsernameError(e));
|
||||
}
|
||||
@@ -75,9 +75,9 @@ function* submitNewUsernameSaga({ payload: username }) {
|
||||
|
||||
function* submitProfileUISaga({ payload }) {
|
||||
try {
|
||||
const response = yield call(putUpdateMyProfileUI, payload);
|
||||
yield put(submitProfileUIComplete({ ...response, payload }));
|
||||
yield put(createFlashMessage(response));
|
||||
const { data } = yield call(putUpdateMyProfileUI, payload);
|
||||
yield put(submitProfileUIComplete({ ...data, payload }));
|
||||
yield put(createFlashMessage(data));
|
||||
} catch (e) {
|
||||
yield put(submitProfileUIError);
|
||||
}
|
||||
@@ -85,10 +85,10 @@ function* submitProfileUISaga({ payload }) {
|
||||
|
||||
function* updateUserFlagSaga({ payload: update }) {
|
||||
try {
|
||||
const response = yield call(putUpdateUserFlag, update);
|
||||
yield put(updateUserFlagComplete({ ...response, payload: update }));
|
||||
const { data } = yield call(putUpdateUserFlag, update);
|
||||
yield put(updateUserFlagComplete({ ...data, payload: update }));
|
||||
yield put(
|
||||
createFlashMessage({ ...response, variables: { theme: update.theme } })
|
||||
createFlashMessage({ ...data, variables: { theme: update.theme } })
|
||||
);
|
||||
} catch (e) {
|
||||
yield put(updateUserFlagError(e));
|
||||
@@ -97,9 +97,9 @@ function* updateUserFlagSaga({ payload: update }) {
|
||||
|
||||
function* updateMySocialsSaga({ payload: update }) {
|
||||
try {
|
||||
const response = yield call(putUpdateMySocials, update);
|
||||
yield put(updateMySocialsComplete({ ...response, payload: update }));
|
||||
yield put(createFlashMessage({ ...response }));
|
||||
const { data } = yield call(putUpdateMySocials, update);
|
||||
yield put(updateMySocialsComplete({ ...data, payload: update }));
|
||||
yield put(createFlashMessage({ ...data }));
|
||||
} catch (e) {
|
||||
yield put(updateMySocialsError);
|
||||
}
|
||||
@@ -108,9 +108,9 @@ function* updateMySocialsSaga({ payload: update }) {
|
||||
function* updateMySoundSaga({ payload: update }) {
|
||||
try {
|
||||
store.set('fcc-sound', !!update.sound);
|
||||
const response = yield call(putUpdateMySound, update);
|
||||
yield put(updateMySoundComplete({ ...response, payload: update }));
|
||||
yield put(createFlashMessage({ ...response }));
|
||||
const { data } = yield call(putUpdateMySound, update);
|
||||
yield put(updateMySoundComplete({ ...data, payload: update }));
|
||||
yield put(createFlashMessage({ ...data }));
|
||||
} catch (e) {
|
||||
yield put(updateMySoundError);
|
||||
}
|
||||
@@ -118,9 +118,9 @@ function* updateMySoundSaga({ payload: update }) {
|
||||
|
||||
function* updateMyThemeSaga({ payload: update }) {
|
||||
try {
|
||||
const response = yield call(putUpdateMyTheme, update);
|
||||
yield put(updateMyThemeComplete({ ...response, payload: update }));
|
||||
yield put(createFlashMessage({ ...response }));
|
||||
const { data } = yield call(putUpdateMyTheme, update);
|
||||
yield put(updateMyThemeComplete({ ...data, payload: update }));
|
||||
yield put(createFlashMessage({ ...data }));
|
||||
} catch (e) {
|
||||
yield put(updateMyThemeError);
|
||||
}
|
||||
@@ -128,9 +128,9 @@ function* updateMyThemeSaga({ payload: update }) {
|
||||
|
||||
function* updateMyHonestySaga({ payload: update }) {
|
||||
try {
|
||||
const response = yield call(putUpdateMyHonesty, update);
|
||||
yield put(updateMyHonestyComplete({ ...response, payload: update }));
|
||||
yield put(createFlashMessage({ ...response }));
|
||||
const { data } = yield call(putUpdateMyHonesty, update);
|
||||
yield put(updateMyHonestyComplete({ ...data, payload: update }));
|
||||
yield put(createFlashMessage({ ...data }));
|
||||
} catch (e) {
|
||||
yield put(updateMyHonestyError);
|
||||
}
|
||||
@@ -138,9 +138,9 @@ function* updateMyHonestySaga({ payload: update }) {
|
||||
|
||||
function* updateMyQuincyEmailSaga({ payload: update }) {
|
||||
try {
|
||||
const response = yield call(putUpdateMyQuincyEmail, update);
|
||||
yield put(updateMyQuincyEmailComplete({ ...response, payload: update }));
|
||||
yield put(createFlashMessage({ ...response }));
|
||||
const { data } = yield call(putUpdateMyQuincyEmail, update);
|
||||
yield put(updateMyQuincyEmailComplete({ ...data, payload: update }));
|
||||
yield put(createFlashMessage({ ...data }));
|
||||
} catch (e) {
|
||||
yield put(updateMyQuincyEmailError);
|
||||
}
|
||||
@@ -148,9 +148,9 @@ function* updateMyQuincyEmailSaga({ payload: update }) {
|
||||
|
||||
function* updateMyPortfolioSaga({ payload: update }) {
|
||||
try {
|
||||
const response = yield call(putUpdateMyPortfolio, update);
|
||||
yield put(updateMyPortfolioComplete({ ...response, payload: update }));
|
||||
yield put(createFlashMessage({ ...response }));
|
||||
const { data } = yield call(putUpdateMyPortfolio, update);
|
||||
yield put(updateMyPortfolioComplete({ ...data, payload: update }));
|
||||
yield put(createFlashMessage({ ...data }));
|
||||
} catch (e) {
|
||||
yield put(updateMyPortfolioError);
|
||||
}
|
||||
@@ -158,7 +158,9 @@ function* updateMyPortfolioSaga({ payload: update }) {
|
||||
|
||||
function* validateUsernameSaga({ payload }) {
|
||||
try {
|
||||
const { exists } = yield call(getUsernameExists, payload);
|
||||
const {
|
||||
data: { exists }
|
||||
} = yield call(getUsernameExists, payload);
|
||||
yield put(validateUsernameComplete(exists));
|
||||
} catch (e) {
|
||||
yield put(validateUsernameError(e));
|
||||
@@ -189,10 +191,9 @@ function* verifyCertificationSaga({ payload }) {
|
||||
|
||||
// redux says challenges are complete, call back end
|
||||
try {
|
||||
const { response, isCertMap, completedChallenges } = yield call(
|
||||
putVerifyCert,
|
||||
payload
|
||||
);
|
||||
const {
|
||||
data: { response, isCertMap, completedChallenges }
|
||||
} = yield call(putVerifyCert, payload);
|
||||
yield put(
|
||||
verifyCertComplete({
|
||||
...response,
|
||||
|
||||
@@ -13,14 +13,14 @@ function* updateMyEmailSaga({ payload: email = '' }) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = yield call(putUserUpdateEmail, email);
|
||||
const { data } = yield call(putUserUpdateEmail, email);
|
||||
yield put(
|
||||
updateMyEmailComplete({
|
||||
...response,
|
||||
...data,
|
||||
payload: { email, isEmailVerified: false }
|
||||
})
|
||||
);
|
||||
yield put(createFlashMessage(response));
|
||||
yield put(createFlashMessage(data));
|
||||
} catch (e) {
|
||||
yield put(updateMyEmailError(e));
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import { showCertComplete, showCertError } from '.';
|
||||
|
||||
function* getShowCertSaga({ payload: { username, certSlug } }) {
|
||||
try {
|
||||
const response = yield call(getShowCert, username, certSlug);
|
||||
const { messages } = response;
|
||||
const { data } = yield call(getShowCert, username, certSlug);
|
||||
const { messages } = data;
|
||||
if (messages && messages.length) {
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
yield put(createFlashMessage(messages[i]));
|
||||
@@ -16,7 +16,7 @@ function* getShowCertSaga({ payload: { username, certSlug } }) {
|
||||
yield call(navigate, '/');
|
||||
return;
|
||||
}
|
||||
yield put(showCertComplete(response));
|
||||
yield put(showCertComplete(data));
|
||||
} catch (e) {
|
||||
yield put(showCertError(e));
|
||||
}
|
||||
|
||||
@@ -17,12 +17,9 @@ const message = {
|
||||
|
||||
function* deleteUserTokenSaga() {
|
||||
try {
|
||||
const response = yield call(deleteUserToken);
|
||||
const { data } = yield call(deleteUserToken);
|
||||
|
||||
if (
|
||||
response &&
|
||||
Object.prototype.hasOwnProperty.call(response, 'userToken')
|
||||
) {
|
||||
if (data && Object.prototype.hasOwnProperty.call(data, 'userToken')) {
|
||||
yield put(deleteUserTokenComplete());
|
||||
yield put(createFlashMessage(message.deleted));
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,8 @@ export function getGuideUrl({ forumTopicId, title = '' }: GuideData): string {
|
||||
: `${forumLocation}/search?q=${title}%20in%3Atitle%20order%3Aviews`;
|
||||
}
|
||||
|
||||
export function isGoodXHRStatus(status: string): boolean {
|
||||
export function isGoodXHRStatus(status?: string): boolean {
|
||||
if (!status) return false;
|
||||
const statusInt = parseInt(status, 10);
|
||||
return (statusInt >= 200 && statusInt < 400) || statusInt === 402;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { post } from '../../../utils/ajax';
|
||||
import type { ResponseWithData } from '../../../utils/ajax';
|
||||
|
||||
interface PostData {
|
||||
endpoint: string;
|
||||
@@ -9,6 +10,6 @@ interface PostData {
|
||||
export default function postUpdate$({
|
||||
endpoint,
|
||||
payload
|
||||
}: PostData): Observable<void> {
|
||||
}: PostData): Observable<ResponseWithData<void>> {
|
||||
return from(post(endpoint, payload));
|
||||
}
|
||||
|
||||
@@ -26,21 +26,42 @@ function getCSRFToken() {
|
||||
return token ?? '';
|
||||
}
|
||||
|
||||
// TODO: Might want to handle flash messages as close to the request as possible
|
||||
// to make use of the Response object (message, status, etc)
|
||||
async function get<T>(path: string): Promise<T> {
|
||||
return fetch(`${base}${path}`, defaultOptions).then<T>(res => res.json());
|
||||
export interface ResponseWithData<T> {
|
||||
response: Response;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export function post<T = void>(path: string, body: unknown): Promise<T> {
|
||||
// TODO: Might want to handle flash messages as close to the request as possible
|
||||
// to make use of the Response object (message, status, etc)
|
||||
async function get<T>(path: string): Promise<ResponseWithData<T>> {
|
||||
const response = await fetch(`${base}${path}`, defaultOptions);
|
||||
|
||||
return combineDataWithResponse(response);
|
||||
}
|
||||
|
||||
async function combineDataWithResponse<T>(response: Response) {
|
||||
const data = (await response.json()) as T;
|
||||
return { response, data };
|
||||
}
|
||||
|
||||
export function post<T = void>(
|
||||
path: string,
|
||||
body: unknown
|
||||
): Promise<ResponseWithData<T>> {
|
||||
return request('POST', path, body);
|
||||
}
|
||||
|
||||
function put<T = void>(path: string, body: unknown): Promise<T> {
|
||||
function put<T = void>(
|
||||
path: string,
|
||||
body: unknown
|
||||
): Promise<ResponseWithData<T>> {
|
||||
return request('PUT', path, body);
|
||||
}
|
||||
|
||||
function deleteRequest<T = void>(path: string, body: unknown): Promise<T> {
|
||||
function deleteRequest<T = void>(
|
||||
path: string,
|
||||
body: unknown
|
||||
): Promise<ResponseWithData<T>> {
|
||||
return request('DELETE', path, body);
|
||||
}
|
||||
|
||||
@@ -48,7 +69,7 @@ async function request<T>(
|
||||
method: 'POST' | 'PUT' | 'DELETE',
|
||||
path: string,
|
||||
body: unknown
|
||||
): Promise<T> {
|
||||
): Promise<ResponseWithData<T>> {
|
||||
const options: RequestInit = {
|
||||
...defaultOptions,
|
||||
method,
|
||||
@@ -58,7 +79,9 @@ async function request<T>(
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
};
|
||||
return fetch(`${base}${path}`, options).then<T>(res => res.json());
|
||||
|
||||
const response = await fetch(`${base}${path}`, options);
|
||||
return combineDataWithResponse(response);
|
||||
}
|
||||
|
||||
/** GET **/
|
||||
@@ -128,17 +151,20 @@ function mapKeyToFileKey<K>(
|
||||
return files.map(({ key, ...rest }) => ({ ...rest, fileKey: key }));
|
||||
}
|
||||
|
||||
export function getSessionUser(): Promise<SessionUser> {
|
||||
const response: Promise<ApiUser & ApiSessionResponse> = get(
|
||||
'/user/get-session-user'
|
||||
);
|
||||
export function getSessionUser(): Promise<ResponseWithData<SessionUser>> {
|
||||
const responseWithData: Promise<
|
||||
ResponseWithData<ApiUser & ApiSessionResponse>
|
||||
> = get('/user/get-session-user');
|
||||
// TODO: Once DB is migrated, no longer need to parse `files` -> `challengeFiles` etc.
|
||||
return response.then(data => {
|
||||
return responseWithData.then(({ response, data }) => {
|
||||
const { result, user } = parseApiResponseToClientUser(data);
|
||||
return {
|
||||
sessionMeta: data.sessionMeta,
|
||||
result,
|
||||
user
|
||||
response,
|
||||
data: {
|
||||
sessionMeta: data.sessionMeta,
|
||||
result,
|
||||
user
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -147,18 +173,23 @@ type UserProfileResponse = {
|
||||
entities: Omit<UserResponse, 'result'>;
|
||||
result: string | undefined;
|
||||
};
|
||||
export function getUserProfile(username: string): Promise<UserProfileResponse> {
|
||||
const response: Promise<{ entities?: ApiUser; result?: string }> = get(
|
||||
export function getUserProfile(
|
||||
username: string
|
||||
): Promise<ResponseWithData<UserProfileResponse>> {
|
||||
const responseWithData = get<{ entities?: ApiUser; result?: string }>(
|
||||
`/api/users/get-public-profile?username=${username}`
|
||||
);
|
||||
return response.then(data => {
|
||||
return responseWithData.then(({ response, data }) => {
|
||||
const { result, user } = parseApiResponseToClientUser({
|
||||
user: data.entities?.user ?? {},
|
||||
result: data.result
|
||||
});
|
||||
return {
|
||||
entities: { user },
|
||||
result
|
||||
response,
|
||||
data: {
|
||||
entities: { user },
|
||||
result
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -169,11 +200,16 @@ interface Cert {
|
||||
date: Date;
|
||||
completionTime: string;
|
||||
}
|
||||
export function getShowCert(username: string, certSlug: string): Promise<Cert> {
|
||||
export function getShowCert(
|
||||
username: string,
|
||||
certSlug: string
|
||||
): Promise<ResponseWithData<Cert>> {
|
||||
return get(`/certificate/showCert/${username}/${certSlug}`);
|
||||
}
|
||||
|
||||
export function getUsernameExists(username: string): Promise<boolean> {
|
||||
export function getUsernameExists(
|
||||
username: string
|
||||
): Promise<ResponseWithData<boolean>> {
|
||||
return get(`/api/users/exists?username=${username}`);
|
||||
}
|
||||
|
||||
@@ -190,44 +226,48 @@ interface Donation {
|
||||
}
|
||||
// TODO: Verify if the body has and needs this Donation type. The api seems to
|
||||
// just need the body to exist, but doesn't seem to use the properties.
|
||||
export function addDonation(body: Donation): Promise<void> {
|
||||
export function addDonation(body: Donation): Promise<ResponseWithData<void>> {
|
||||
return post('/donate/add-donation', body);
|
||||
}
|
||||
|
||||
export function postChargeStripe(body: Donation): Promise<void> {
|
||||
export function postChargeStripe(
|
||||
body: Donation
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return post('/donate/charge-stripe', body);
|
||||
}
|
||||
|
||||
export function postChargeStripeCard(body: Donation): Promise<void> {
|
||||
export function postChargeStripeCard(
|
||||
body: Donation
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return post('/donate/charge-stripe-card', body);
|
||||
}
|
||||
interface Report {
|
||||
username: string;
|
||||
reportDescription: string;
|
||||
}
|
||||
export function postReportUser(body: Report): Promise<void> {
|
||||
export function postReportUser(body: Report): Promise<ResponseWithData<void>> {
|
||||
return post('/user/report-user', body);
|
||||
}
|
||||
|
||||
// Both are called without a payload in danger-zone-saga,
|
||||
// which suggests both are sent without any body
|
||||
// TODO: Convert to DELETE
|
||||
export function postDeleteAccount(): Promise<void> {
|
||||
export function postDeleteAccount(): Promise<ResponseWithData<void>> {
|
||||
return post('/account/delete', {});
|
||||
}
|
||||
|
||||
export function postResetProgress(): Promise<void> {
|
||||
export function postResetProgress(): Promise<ResponseWithData<void>> {
|
||||
return post('/account/reset-progress', {});
|
||||
}
|
||||
|
||||
export function postUserToken(): Promise<void> {
|
||||
export function postUserToken(): Promise<ResponseWithData<void>> {
|
||||
return post('/user/user-token', {});
|
||||
}
|
||||
|
||||
export function postSaveChallenge(body: {
|
||||
id: string;
|
||||
files: ChallengeFiles;
|
||||
}): Promise<void> {
|
||||
}): Promise<ResponseWithData<void>> {
|
||||
return post('/save-challenge', body);
|
||||
}
|
||||
|
||||
@@ -239,17 +279,21 @@ interface MyAbout {
|
||||
about: string;
|
||||
picture: string;
|
||||
}
|
||||
export function putUpdateMyAbout(values: MyAbout): Promise<void> {
|
||||
export function putUpdateMyAbout(
|
||||
values: MyAbout
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-about', { ...values });
|
||||
}
|
||||
|
||||
export function putUpdateMyUsername(username: string): Promise<void> {
|
||||
export function putUpdateMyUsername(
|
||||
username: string
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-username', { username });
|
||||
}
|
||||
|
||||
export function putUpdateMyProfileUI(
|
||||
profileUI: User['profileUI']
|
||||
): Promise<void> {
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-profileui', { profileUI });
|
||||
}
|
||||
|
||||
@@ -258,59 +302,65 @@ export function putUpdateMyProfileUI(
|
||||
// https://stackoverflow.com/a/60807986
|
||||
export function putUpdateUserFlag(
|
||||
update: Record<string, string>
|
||||
): Promise<void> {
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-user-flag', update);
|
||||
}
|
||||
|
||||
export function putUpdateMySocials(
|
||||
update: Record<string, string>
|
||||
): Promise<void> {
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-socials', update);
|
||||
}
|
||||
|
||||
export function putUpdateMySound(
|
||||
update: Record<string, string>
|
||||
): Promise<void> {
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-sound', update);
|
||||
}
|
||||
|
||||
export function putUpdateMyTheme(
|
||||
update: Record<string, string>
|
||||
): Promise<void> {
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-theme', update);
|
||||
}
|
||||
|
||||
export function putUpdateMyHonesty(
|
||||
update: Record<string, string>
|
||||
): Promise<void> {
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-honesty', update);
|
||||
}
|
||||
|
||||
export function putUpdateMyQuincyEmail(
|
||||
update: Record<string, string>
|
||||
): Promise<void> {
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-quincy-email', update);
|
||||
}
|
||||
|
||||
export function putUpdateMyPortfolio(
|
||||
update: Record<string, string>
|
||||
): Promise<void> {
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-portfolio', update);
|
||||
}
|
||||
|
||||
export function putUserAcceptsTerms(quincyEmails: boolean): Promise<void> {
|
||||
export function putUserAcceptsTerms(
|
||||
quincyEmails: boolean
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-privacy-terms', { quincyEmails });
|
||||
}
|
||||
|
||||
export function putUserUpdateEmail(email: string): Promise<void> {
|
||||
export function putUserUpdateEmail(
|
||||
email: string
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/update-my-email', { email });
|
||||
}
|
||||
|
||||
export function putVerifyCert(certSlug: string): Promise<void> {
|
||||
export function putVerifyCert(
|
||||
certSlug: string
|
||||
): Promise<ResponseWithData<void>> {
|
||||
return put('/certificate/verify', { certSlug });
|
||||
}
|
||||
|
||||
/** DELETE **/
|
||||
export function deleteUserToken(): Promise<void> {
|
||||
export function deleteUserToken(): Promise<ResponseWithData<void>> {
|
||||
return deleteRequest('/user/user-token', {});
|
||||
}
|
||||
|
||||
59
cypress/integration/learn/challenges/failed-updates.js
Normal file
59
cypress/integration/learn/challenges/failed-updates.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const store = require('store');
|
||||
|
||||
const failedUpdates = [
|
||||
{
|
||||
endpoint: '/modern-challenge-completed',
|
||||
payload: { id: '5dc1798ff86c76b9248c6eb3', challengeType: 0 },
|
||||
id: '4bd1d704-cfaa-44f7-92a3-bc0d857dbaa6'
|
||||
},
|
||||
{
|
||||
endpoint: '/modern-challenge-completed',
|
||||
payload: { id: '5dc17d3bf86c76b9248c6eb4', challengeType: 0 },
|
||||
id: 'ea289e2f-a5d2-45e0-b795-0f9f4afc5124'
|
||||
}
|
||||
];
|
||||
|
||||
const failedUpdatesKey = 'fcc-failed-updates';
|
||||
|
||||
describe('failed update flushing', () => {
|
||||
before(() => {
|
||||
cy.exec('npm run seed');
|
||||
cy.login();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.preserveSession();
|
||||
});
|
||||
|
||||
it('should resubmit failed updates, check they are stored, then flush', () => {
|
||||
store.set(failedUpdatesKey, failedUpdates);
|
||||
cy.request('http://localhost:3000/user/get-session-user')
|
||||
.its('body.user.developmentuser.completedChallenges')
|
||||
.then(completedChallenges => {
|
||||
const completedIds = completedChallenges.map(challenge => challenge.id);
|
||||
|
||||
failedUpdates.forEach(failedUpdate => {
|
||||
expect(completedIds).not.to.include(failedUpdate.payload.id);
|
||||
});
|
||||
});
|
||||
|
||||
cy.intercept('http://localhost:3000/modern-challenge-completed').as(
|
||||
'completed'
|
||||
);
|
||||
cy.wrap(store.get(failedUpdatesKey)).should('deep.equal', failedUpdates);
|
||||
cy.reload();
|
||||
cy.wait('@completed');
|
||||
// if we don't wait for both requests to complete, we have a race condition
|
||||
cy.wait('@completed');
|
||||
cy.request('http://localhost:3000/user/get-session-user')
|
||||
.its('body.user.developmentuser.completedChallenges')
|
||||
.then(completedChallenges => {
|
||||
const completedIds = completedChallenges.map(challenge => challenge.id);
|
||||
|
||||
failedUpdates.forEach(failedUpdate => {
|
||||
expect(completedIds).to.include(failedUpdate.payload.id);
|
||||
});
|
||||
expect(store.get(failedUpdatesKey)).to.be.empty;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user